#!/usr/bin/env/python # # Copyright 2011 The Apache Software Foundation # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF 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 try: from xml.etree import ElementTree except ImportError: print "Building release notes is not supported on this platform." sys.exit(0) 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))