relnotes.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. #!/usr/bin/python
  2. # Licensed under the Apache License, Version 2.0 (the "License");
  3. # you may not use this file except in compliance with the License.
  4. # You may obtain a copy of the License at
  5. #
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. #
  8. # Unless required by applicable law or agreed to in writing, software
  9. # distributed under the License is distributed on an "AS IS" BASIS,
  10. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. # See the License for the specific language governing permissions and
  12. # limitations under the License.
  13. import re
  14. import sys
  15. from optparse import OptionParser
  16. import httplib
  17. import urllib
  18. import cgi
  19. try:
  20. import json
  21. except ImportError:
  22. import simplejson as json
  23. namePattern = re.compile(r' \([0-9]+\)')
  24. def clean(str):
  25. return quoteHtml(re.sub(namePattern, "", str))
  26. def formatComponents(str):
  27. str = re.sub(namePattern, '', str).replace("'", "")
  28. if str != "":
  29. ret = "(" + str + ")"
  30. else:
  31. ret = ""
  32. return quoteHtml(ret)
  33. def quoteHtml(str):
  34. return cgi.escape(str).encode('ascii', 'xmlcharrefreplace')
  35. def mstr(obj):
  36. if (obj == None):
  37. return ""
  38. return unicode(obj)
  39. class Version:
  40. """Represents a version number"""
  41. def __init__(self, data):
  42. self.mod = False
  43. self.data = data
  44. found = re.match('^((\d+)(\.\d+)*).*$', data)
  45. if (found):
  46. self.parts = [ int(p) for p in found.group(1).split('.') ]
  47. else:
  48. self.parts = []
  49. # backfill version with zeroes if missing parts
  50. self.parts.extend((0,) * (3 - len(self.parts)))
  51. def decBugFix(self):
  52. self.mod = True
  53. self.parts[2] -= 1
  54. return self
  55. def __str__(self):
  56. if (self.mod):
  57. return '.'.join([ str(p) for p in self.parts ])
  58. return self.data
  59. def __cmp__(self, other):
  60. return cmp(self.parts, other.parts)
  61. class Jira:
  62. """A single JIRA"""
  63. def __init__(self, data, parent):
  64. self.key = data['key']
  65. self.fields = data['fields']
  66. self.parent = parent
  67. self.notes = None
  68. def getId(self):
  69. return mstr(self.key)
  70. def getDescription(self):
  71. return mstr(self.fields['description'])
  72. def getReleaseNote(self):
  73. if (self.notes == None):
  74. field = self.parent.fieldIdMap['Release Note']
  75. if (self.fields.has_key(field)):
  76. self.notes=mstr(self.fields[field])
  77. else:
  78. self.notes=self.getDescription()
  79. return self.notes
  80. def getPriority(self):
  81. ret = ""
  82. pri = self.fields['priority']
  83. if(pri != None):
  84. ret = pri['name']
  85. return mstr(ret)
  86. def getAssignee(self):
  87. ret = ""
  88. mid = self.fields['assignee']
  89. if(mid != None):
  90. ret = mid['displayName']
  91. return mstr(ret)
  92. def getComponents(self):
  93. return " , ".join([ comp['name'] for comp in self.fields['components'] ])
  94. def getSummary(self):
  95. return self.fields['summary']
  96. def getType(self):
  97. ret = ""
  98. mid = self.fields['issuetype']
  99. if(mid != None):
  100. ret = mid['name']
  101. return mstr(ret)
  102. def getReporter(self):
  103. ret = ""
  104. mid = self.fields['reporter']
  105. if(mid != None):
  106. ret = mid['displayName']
  107. return mstr(ret)
  108. def getProject(self):
  109. ret = ""
  110. mid = self.fields['project']
  111. if(mid != None):
  112. ret = mid['key']
  113. return mstr(ret)
  114. class JiraIter:
  115. """An Iterator of JIRAs"""
  116. def __init__(self, versions):
  117. self.versions = versions
  118. resp = urllib.urlopen("https://issues.apache.org/jira/rest/api/2/field")
  119. data = json.loads(resp.read())
  120. self.fieldIdMap = {}
  121. for part in data:
  122. self.fieldIdMap[part['name']] = part['id']
  123. self.jiras = []
  124. at=0
  125. end=1
  126. count=100
  127. while (at < end):
  128. params = urllib.urlencode({'jql': "project in (HADOOP,HDFS,MAPREDUCE,YARN) and fixVersion in ('"+"' , '".join(versions)+"') and resolution = Fixed", 'startAt':at+1, 'maxResults':count})
  129. resp = urllib.urlopen("https://issues.apache.org/jira/rest/api/2/search?%s"%params)
  130. data = json.loads(resp.read())
  131. if (data.has_key('errorMessages')):
  132. raise Exception(data['errorMessages'])
  133. at = data['startAt'] + data['maxResults']
  134. end = data['total']
  135. self.jiras.extend(data['issues'])
  136. self.iter = self.jiras.__iter__()
  137. def __iter__(self):
  138. return self
  139. def next(self):
  140. data = self.iter.next()
  141. j = Jira(data, self)
  142. return j
  143. class Outputs:
  144. """Several different files to output to at the same time"""
  145. def __init__(self, base_file_name, file_name_pattern, keys, params={}):
  146. self.params = params
  147. self.base = open(base_file_name%params, 'w')
  148. self.others = {}
  149. for key in keys:
  150. both = dict(params)
  151. both['key'] = key
  152. self.others[key] = open(file_name_pattern%both, 'w')
  153. def writeAll(self, pattern):
  154. both = dict(self.params)
  155. both['key'] = ''
  156. self.base.write(pattern%both)
  157. for key in self.others.keys():
  158. both = dict(self.params)
  159. both['key'] = key
  160. self.others[key].write(pattern%both)
  161. def writeKeyRaw(self, key, str):
  162. self.base.write(str)
  163. if (self.others.has_key(key)):
  164. self.others[key].write(str)
  165. def close(self):
  166. self.base.close()
  167. for fd in self.others.values():
  168. fd.close()
  169. def main():
  170. parser = OptionParser(usage="usage: %prog [options] [USER-ignored] [PASSWORD-ignored] [VERSION]")
  171. parser.add_option("-v", "--version", dest="versions",
  172. action="append", type="string",
  173. help="versions in JIRA to include in releasenotes", metavar="VERSION")
  174. parser.add_option("--previousVer", dest="previousVer",
  175. action="store", type="string",
  176. help="previous version to include in releasenotes", metavar="VERSION")
  177. (options, args) = parser.parse_args()
  178. if (options.versions == None):
  179. options.versions = []
  180. if (len(args) > 2):
  181. options.versions.append(args[2])
  182. if (len(options.versions) <= 0):
  183. parser.error("At least one version needs to be supplied")
  184. versions = [ Version(v) for v in options.versions];
  185. versions.sort();
  186. maxVersion = str(versions[-1])
  187. if(options.previousVer == None):
  188. options.previousVer = str(versions[0].decBugFix())
  189. print >> sys.stderr, "WARNING: no previousVersion given, guessing it is "+options.previousVer
  190. list = JiraIter(options.versions)
  191. version = maxVersion
  192. outputs = Outputs("releasenotes.%(ver)s.html",
  193. "releasenotes.%(key)s.%(ver)s.html",
  194. ["HADOOP","HDFS","MAPREDUCE","YARN"], {"ver":maxVersion, "previousVer":options.previousVer})
  195. head = '<META http-equiv="Content-Type" content="text/html; charset=UTF-8">\n' \
  196. '<title>Hadoop %(key)s %(ver)s Release Notes</title>\n' \
  197. '<STYLE type="text/css">\n' \
  198. ' H1 {font-family: sans-serif}\n' \
  199. ' H2 {font-family: sans-serif; margin-left: 7mm}\n' \
  200. ' TABLE {margin-left: 7mm}\n' \
  201. '</STYLE>\n' \
  202. '</head>\n' \
  203. '<body>\n' \
  204. '<h1>Hadoop %(key)s %(ver)s Release Notes</h1>\n' \
  205. 'These release notes include new developer and user-facing incompatibilities, features, and major improvements. \n' \
  206. '<a name="changes"/>\n' \
  207. '<h2>Changes since Hadoop %(previousVer)s</h2>\n' \
  208. '<ul>\n'
  209. outputs.writeAll(head)
  210. for jira in list:
  211. line = '<li> <a href="https://issues.apache.org/jira/browse/%s">%s</a>.\n' \
  212. ' %s %s reported by %s and fixed by %s %s<br>\n' \
  213. ' <b>%s</b><br>\n' \
  214. ' <blockquote>%s</blockquote></li>\n' \
  215. % (quoteHtml(jira.getId()), quoteHtml(jira.getId()), clean(jira.getPriority()), clean(jira.getType()).lower(),
  216. quoteHtml(jira.getReporter()), quoteHtml(jira.getAssignee()), formatComponents(jira.getComponents()),
  217. quoteHtml(jira.getSummary()), quoteHtml(jira.getReleaseNote()))
  218. outputs.writeKeyRaw(jira.getProject(), line)
  219. outputs.writeAll("</ul>\n</body></html>\n")
  220. outputs.close()
  221. if __name__ == "__main__":
  222. main()