diff --git a/build.xml b/build.xml index 7a1c6ab5..35d59877 100644 --- a/build.xml +++ b/build.xml @@ -31,11 +31,20 @@ + + + + + + + + + @@ -79,6 +88,10 @@ + + + @@ -720,11 +733,29 @@ - + + + + + + + + + + + + + + + + + diff --git a/src/scripts/relnotes.py b/src/scripts/relnotes.py new file mode 100644 index 00000000..311b5770 --- /dev/null +++ b/src/scripts/relnotes.py @@ -0,0 +1,246 @@ +#!/usr/bin/env/python +# +# Licensed to Cloudera, Inc. under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# Cloudera, Inc. licenses this file to You 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. +# +# +# +# Generates Apache-style release notes in an HTML file +# for a specific commit range. +# +# Run with '-h' to see usage. + +import datetime +import os +import re +import sys + +from xml.etree import ElementTree + + +NUM_ARGS = 6 + +def print_usage(prgm_name): + """ Print the usage for this program """ + print "Usage: " + prgm_name + " " \ + + " " + print "" + print " : Directory where release notes should be written to." + print " : Root of the git repository to collect info from." + print " : What set of commits form this release." + print " : The version number to print in the release notes." + print " : The previous release version number." + + +def get_log(git_dir, commit_range): + """ Return the set of lines corresponding to the git log for the specified + commit range. + """ + + os.chdir(git_dir) + cmd = "git log --no-color '--pretty=format:%s' '" + commit_range + "'" + return os.popen(cmd).readlines() + + +def sanitize_log(in_log): + """ 'sanitize' the log. + Some entries do not have a separate subject and body by accident. + Return a new log that only includes the first sentence of each + subject. (Note that we also usually have a 'SQOOP-nn.' before this + sentence.) + """ + out_log = [] + for line in in_log: + line = line.strip() + sentences = line.split(". ") + if len(sentences) <= 2: + out_log.append(line) # Unchanged original input. + else: + out_log.append(sentences[0] + ". " + sentences[1] + ".") + + return out_log + + +def get_jira_doc(issue): + """ Get the XML document from JIRA for a specified issue. """ + + xml = os.popen("curl -s 'https://issues.cloudera.org/si/jira.issueviews:" \ + + "issue-xml/%s/%s.xml?field=key&field=type&field=parent'" % (issue, issue)).read() + return ElementTree.fromstring(xml) + + +def get_jira_issue_types(log): + """ Return a dict from issue-type -> ((issue-name, summary) list) by looking + up the issues in our JIRA. + """ + + d = {} + + def add_issue(issue, typ, line): + try: + d[typ].append((issue, line)) + except KeyError: + # This issue type hasn't been seen yet. Add a new list. + d[typ] = [ (issue, line) ] + + jira_reg = r"^(SQOOP-\d+)" + for line in log: + matched_line = False + for m in re.finditer(jira_reg, line, re.M): + matched_line = True + jira = m.group(1) + doc = get_jira_doc(jira) + issue_type = doc.find('./channel/item/type').text + # Subtasks use the type of their parent item. + if issue_type == "Sub-task": + parent_doc = get_jira_doc(doc.find('./channel/item/parent').text) + issue_type = parent_doc.find('./channel/item/type').text + + add_issue(jira, issue_type, line) + if not matched_line and not line.startswith("CLOUDERA-BUILD."): + # This line did not start with "SQOOP-.." + # Unless it's a CDH buildfix, add it in as a "Task". + add_issue("", "Task", line) + + return d + + +def get_date(): + """ Return the current month and year formatted as a string. """ + return datetime.date.today().strftime("%B, %Y") + + +def add_links(summary_line): + """ Given a line like "SQOOP-40. Do something", add links to the JIRA + and any appropriate SIPs, and return the line with links. + """ + + initial_jira_reg = r"^(SQOOP-\d+)\. (.*)" + + # Reformat the issue id away from the summary. + m = re.match(initial_jira_reg, summary_line) + if m == None: + # Line in unexpected format. Return as-is. + return summary_line + jira = m.group(1) + text = m.group(2) + + # Add links to JIRA and SIP wiki. + + issue_reg = r"(SQOOP-\d+)" + issue_subst = r'\1' + + sip_reg = r"(SIP-\d+)" + sip_subst = r'\1' + + output = "[" + jira + "] - " + text + output = re.sub(issue_reg, issue_subst, output) + output = re.sub(sip_reg, sip_subst, output) + + return output + + +__user_types = { + "Bug" : "Bug fixes", + "Improvement" : "Improvements", + "New Feature" : "New features", + "Task" : "Tasks" +} + +def user_issue_type(typ): + """ Return a user-friendly issue type string based on the JIRA issue + type string. + """ + global __user_types + + try: + return __user_types[typ] + except KeyError: + # If we don't have a plural-form string set, just use the input. + return typ + + + +def format_html(newversion, oldversion, log, jira_info): + """ Creates the HTML representation of the release notes and returns + it as a string. + """ + + output_lines = [] + output_lines.append(""" + +Sqoop %(newversion)s Release Notes + +

Release Notes for Sqoop %(newversion)s: %(date)s

+ +

This document lists all Sqoop issues included in version %(newversion)s +not present in the previous release, %(oldversion)s.

+""" % { "newversion" : newversion, + "oldversion" : oldversion, + "date" : get_date() }) + + + # Sort the output list by issue type. + types = jira_info.keys() + types.sort() + for typ in types: + output_lines.append("

" + user_issue_type(typ) + ":

    \n") + for (issue, summary) in jira_info[typ]: + output_lines.append("
  • ") + output_lines.append(add_links(summary)) + output_lines.append("
  • \n") + output_lines.append("
\n") + + output_lines.append("\n") + return "".join(output_lines) + + +def main(argv): + if len(argv) > 1 and argv[1] == '-h': + print_usage(argv[0]) + return 0 + + if len(argv) < NUM_ARGS: + print "Missing required argument(s). Try " + argv[0] + " -h" + return 1 + + target_dir = os.path.abspath(os.path.expanduser(argv[1])) + git_src = os.path.abspath(os.path.expanduser(argv[2])) + commit_range = argv[3] + newversion = argv[4] + oldversion = argv[5] + + log = get_log(git_src, commit_range) + log = sanitize_log(log) + jira_info = get_jira_issue_types(log) + html = format_html(newversion, oldversion, log, jira_info) + + os.system("mkdir -p \"" + target_dir + "\"") + handle = open(os.path.join(target_dir, \ + "sqoop-" + newversion + ".releasenotes.html"), "w") + handle.write(html) + handle.close() + + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) + +