| #!/usr/bin/python |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import re |
| import sys |
| from optparse import OptionParser |
| import httplib |
| import urllib |
| import cgi |
| try: |
| import json |
| except ImportError: |
| import simplejson as json |
| |
| |
| namePattern = re.compile(r' \([0-9]+\)') |
| |
| def clean(str): |
| return quoteHtml(re.sub(namePattern, "", str)) |
| |
| def formatComponents(str): |
| str = re.sub(namePattern, '', str).replace("'", "") |
| if str != "": |
| ret = "(" + str + ")" |
| else: |
| ret = "" |
| return quoteHtml(ret) |
| |
| def quoteHtml(str): |
| return cgi.escape(str).encode('ascii', 'xmlcharrefreplace') |
| |
| def mstr(obj): |
| if (obj == None): |
| return "" |
| return unicode(obj) |
| |
| class Version: |
| """Represents a version number""" |
| def __init__(self, data): |
| self.mod = False |
| self.data = data |
| found = re.match('^((\d+)(\.\d+)*).*$', data) |
| if (found): |
| self.parts = [ int(p) for p in found.group(1).split('.') ] |
| else: |
| self.parts = [] |
| # backfill version with zeroes if missing parts |
| self.parts.extend((0,) * (3 - len(self.parts))) |
| |
| def decBugFix(self): |
| self.mod = True |
| self.parts[2] -= 1 |
| return self |
| |
| def __str__(self): |
| if (self.mod): |
| return '.'.join([ str(p) for p in self.parts ]) |
| return self.data |
| |
| def __cmp__(self, other): |
| return cmp(self.parts, other.parts) |
| |
| class Jira: |
| """A single JIRA""" |
| |
| def __init__(self, data, parent): |
| self.key = data['key'] |
| self.fields = data['fields'] |
| self.parent = parent |
| self.notes = None |
| |
| def getId(self): |
| return mstr(self.key) |
| |
| def getDescription(self): |
| return mstr(self.fields['description']) |
| |
| def getReleaseNote(self): |
| if (self.notes == None): |
| field = self.parent.fieldIdMap['Release Note'] |
| if (self.fields.has_key(field)): |
| self.notes=mstr(self.fields[field]) |
| else: |
| self.notes=self.getDescription() |
| return self.notes |
| |
| def getPriority(self): |
| ret = "" |
| pri = self.fields['priority'] |
| if(pri != None): |
| ret = pri['name'] |
| return mstr(ret) |
| |
| def getAssignee(self): |
| ret = "" |
| mid = self.fields['assignee'] |
| if(mid != None): |
| ret = mid['displayName'] |
| return mstr(ret) |
| |
| def getComponents(self): |
| return " , ".join([ comp['name'] for comp in self.fields['components'] ]) |
| |
| def getSummary(self): |
| return self.fields['summary'] |
| |
| def getType(self): |
| ret = "" |
| mid = self.fields['issuetype'] |
| if(mid != None): |
| ret = mid['name'] |
| return mstr(ret) |
| |
| def getReporter(self): |
| ret = "" |
| mid = self.fields['reporter'] |
| if(mid != None): |
| ret = mid['displayName'] |
| return mstr(ret) |
| |
| def getProject(self): |
| ret = "" |
| mid = self.fields['project'] |
| if(mid != None): |
| ret = mid['key'] |
| return mstr(ret) |
| |
| |
| |
| class JiraIter: |
| """An Iterator of JIRAs""" |
| |
| def __init__(self, versions): |
| self.versions = versions |
| |
| resp = urllib.urlopen("https://issues.apache.org/jira/rest/api/2/field") |
| data = json.loads(resp.read()) |
| |
| self.fieldIdMap = {} |
| for part in data: |
| self.fieldIdMap[part['name']] = part['id'] |
| |
| self.jiras = [] |
| at=0 |
| end=1 |
| count=100 |
| while (at < end): |
| params = urllib.urlencode({'jql': "project in (HADOOP,HDFS,MAPREDUCE,YARN) and fixVersion in ('"+"' , '".join(versions)+"') and resolution = Fixed", 'startAt':at, 'maxResults':count}) |
| resp = urllib.urlopen("https://issues.apache.org/jira/rest/api/2/search?%s"%params) |
| data = json.loads(resp.read()) |
| if (data.has_key('errorMessages')): |
| raise Exception(data['errorMessages']) |
| at = data['startAt'] + data['maxResults'] |
| end = data['total'] |
| self.jiras.extend(data['issues']) |
| |
| self.iter = self.jiras.__iter__() |
| |
| def __iter__(self): |
| return self |
| |
| def next(self): |
| data = self.iter.next() |
| j = Jira(data, self) |
| return j |
| |
| class Outputs: |
| """Several different files to output to at the same time""" |
| |
| def __init__(self, base_file_name, file_name_pattern, keys, params={}): |
| self.params = params |
| self.base = open(base_file_name%params, 'w') |
| self.others = {} |
| for key in keys: |
| both = dict(params) |
| both['key'] = key |
| self.others[key] = open(file_name_pattern%both, 'w') |
| |
| def writeAll(self, pattern): |
| both = dict(self.params) |
| both['key'] = '' |
| self.base.write(pattern%both) |
| for key in self.others.keys(): |
| both = dict(self.params) |
| both['key'] = key |
| self.others[key].write(pattern%both) |
| |
| def writeKeyRaw(self, key, str): |
| self.base.write(str) |
| if (self.others.has_key(key)): |
| self.others[key].write(str) |
| |
| def close(self): |
| self.base.close() |
| for fd in self.others.values(): |
| fd.close() |
| |
| def main(): |
| parser = OptionParser(usage="usage: %prog [options] [USER-ignored] [PASSWORD-ignored] [VERSION]") |
| parser.add_option("-v", "--version", dest="versions", |
| action="append", type="string", |
| help="versions in JIRA to include in releasenotes", metavar="VERSION") |
| parser.add_option("--previousVer", dest="previousVer", |
| action="store", type="string", |
| help="previous version to include in releasenotes", metavar="VERSION") |
| |
| (options, args) = parser.parse_args() |
| |
| if (options.versions == None): |
| options.versions = [] |
| |
| if (len(args) > 2): |
| options.versions.append(args[2]) |
| |
| if (len(options.versions) <= 0): |
| parser.error("At least one version needs to be supplied") |
| |
| versions = [ Version(v) for v in options.versions]; |
| versions.sort(); |
| |
| maxVersion = str(versions[-1]) |
| if(options.previousVer == None): |
| options.previousVer = str(versions[0].decBugFix()) |
| print >> sys.stderr, "WARNING: no previousVersion given, guessing it is "+options.previousVer |
| |
| list = JiraIter(options.versions) |
| version = maxVersion |
| outputs = Outputs("releasenotes.%(ver)s.html", |
| "releasenotes.%(key)s.%(ver)s.html", |
| ["HADOOP","HDFS","MAPREDUCE","YARN"], {"ver":maxVersion, "previousVer":options.previousVer}) |
| |
| head = '<META http-equiv="Content-Type" content="text/html; charset=UTF-8">\n' \ |
| '<title>Hadoop %(key)s %(ver)s Release Notes</title>\n' \ |
| '<STYLE type="text/css">\n' \ |
| ' H1 {font-family: sans-serif}\n' \ |
| ' H2 {font-family: sans-serif; margin-left: 7mm}\n' \ |
| ' TABLE {margin-left: 7mm}\n' \ |
| '</STYLE>\n' \ |
| '</head>\n' \ |
| '<body>\n' \ |
| '<h1>Hadoop %(key)s %(ver)s Release Notes</h1>\n' \ |
| 'These release notes include new developer and user-facing incompatibilities, features, and major improvements. \n' \ |
| '<a name="changes"/>\n' \ |
| '<h2>Changes since Hadoop %(previousVer)s</h2>\n' \ |
| '<ul>\n' |
| |
| outputs.writeAll(head) |
| |
| for jira in list: |
| line = '<li> <a href="https://issues.apache.org/jira/browse/%s">%s</a>.\n' \ |
| ' %s %s reported by %s and fixed by %s %s<br>\n' \ |
| ' <b>%s</b><br>\n' \ |
| ' <blockquote>%s</blockquote></li>\n' \ |
| % (quoteHtml(jira.getId()), quoteHtml(jira.getId()), clean(jira.getPriority()), clean(jira.getType()).lower(), |
| quoteHtml(jira.getReporter()), quoteHtml(jira.getAssignee()), formatComponents(jira.getComponents()), |
| quoteHtml(jira.getSummary()), quoteHtml(jira.getReleaseNote())) |
| outputs.writeKeyRaw(jira.getProject(), line) |
| |
| outputs.writeAll("</ul>\n</body></html>\n") |
| outputs.close() |
| |
| if __name__ == "__main__": |
| main() |
| |