| #! /usr/bin/env python |
| # |
| # 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. |
| # |
| |
| # Svn2Info - Create a HTML file with info about a revision range and connect it with a Bugzilla infos |
| # |
| # Usage: |
| # python svnlog2info.py [svnurl|branchname] minrev maxrev [enduser|developer] |
| # |
| # Example: |
| # python svnlog2info.py trunk 1405864 1418409 developer |
| # python svnlog2info.py http://svn.apache.org/repos/asf/openoffice/trunk 1405864 1418409 enduser |
| |
| import sys |
| import re |
| import codecs |
| import xmlrpclib |
| from subprocess import Popen, PIPE |
| from xml.dom.minidom import parseString |
| from xml.sax.saxutils import escape, quoteattr |
| |
| # string constants specific to the Apache OpenOffice project |
| # adjust them to your project's needs |
| svn_default_root_url = "http://svn.apache.org/repos/asf/openoffice/" |
| svn_viewrev_url_base = "http://svn.apache.org/viewvc?view=revision&revision=%d" |
| bzsoap = "https://issues.apache.org/ooo/xmlrpc.cgi" |
| bugref_url = "https://issues.apache.org/ooo/show_bug.cgi?id=" |
| |
| issue_pattern = "^\s*(?:re)?(?:fix)?\s*(?:for)?\s*(?:bug|issue|problem)?\s*#?i?([1-9][0-9][0-9][0-9]+)[#:, ]" |
| infoout_name = "izlist.htm" |
| |
| |
| class Revision(object): |
| """Constructor for a Revision object""" |
| def __init__( self, revnum, author, revlog): |
| self.revnum = revnum |
| self.author = author |
| self.log = revlog |
| self.issue = get_issue( revlog) |
| |
| |
| def get_issue( revlog): |
| """Get the issue number referenced in a commit summary""" |
| issue_re = re.compile( issue_pattern, re.IGNORECASE) |
| issue_match = issue_re.search( revlog) |
| if not issue_match: |
| return None |
| issue_id = int(issue_match.group(1)) |
| return issue_id |
| |
| |
| def get_svn_log( svnurl, revmin_name, revmax_name): |
| """Run the svn log command for the requested revision range""" |
| svncmd = "svn log --xml -r%s:%s %s" % (revmin_name, revmax_name, svnurl) |
| svnproc = Popen( svncmd, shell=True, stdout=PIPE, close_fds=True) |
| svnout = svnproc.communicate() |
| if svnproc.returncode != 0: |
| raise Exception( "SVN LOG failure %d for \"%s\" with \"%s\"" % (svnproc.returncode,svncmd,svnout[1])) |
| return svnout[0] |
| |
| |
| def parse_svn_log_xml( svnout): |
| """Parse the output of the xml-formatted svn log command""" |
| all_revs = [] |
| |
| dom = parseString( svnout) |
| for log in dom.getElementsByTagName('logentry'): |
| revnum = int(log.getAttribute("revision")) |
| author = log.getElementsByTagName("author")[0].firstChild.nodeValue |
| cmtnode = log.getElementsByTagName("msg")[0].firstChild |
| if cmtnode: |
| comment = cmtnode.nodeValue |
| else: |
| comment = "UNCOMMENTED CHANGE" |
| all_revs.append( Revision( revnum, author, comment)) |
| |
| return all_revs |
| |
| |
| def get_bug_details( bugs_to_get): |
| proxy = xmlrpclib.ServerProxy( bzsoap, verbose=False) |
| # try to get all bug details at once |
| try: |
| soaprc = proxy.Bug.get( {"ids" : bugs_to_get}) |
| return soaprc |
| except xmlrpclib.Fault as err: |
| print( err) |
| print( "Problem getting all issue details at once. Retrying each issue individually.") |
| # getting the bug details individually |
| soaprc = {"bugs":[], "faults":[]} |
| for one_id in bugs_to_get: |
| try: |
| one_bug = proxy.Bug.get( {"ids":[one_id]})["bugs"][0] |
| soaprc["bugs"].append( one_bug.copy()) |
| except xmlrpclib.Fault as err: |
| print( 'ignoring #i%d# because "%s"' % (one_id,err.faultString)) |
| soaprc["faults"].append( one_id) |
| |
| return soaprc |
| |
| |
| def revs2info( htmlname, detail_level, all_revs, svnurl, revmin_name, revmax_name): |
| """Create a HTML file with infos about revision range and its referenced issues""" |
| # emit html header to the info file |
| htmlfile = codecs.open( htmlname, "wb", encoding='utf-8') |
| branchname = svnurl.split("/")[-1] |
| header = "<html><head><meta charset=\"utf-8\"></head>\n" |
| revmin_number = all_revs[+0].revnum |
| revmax_number = all_revs[-1].revnum |
| revmin_url = svn_viewrev_url_base % (revmin_number) |
| revmax_url = svn_viewrev_url_base % (revmax_number) |
| header += "<title>Annotated Log for %s..%s</title>\n" % (revmin_name, revmax_name) |
| header += "<body><h1>Revisions <a href=\"%s\">%d</a>..<a href=\"%s\">%d</a> from <a href=\"%s\">%s</a></h1>\n" % (revmin_url, revmin_number, revmax_url, revmax_number, svnurl, branchname) |
| htmlfile.write( header) |
| |
| # split revisions with issue references from other revisions |
| bugid_map = {} |
| other_revs = [] |
| for rev in all_revs: |
| if rev.issue: |
| if not rev.issue in bugid_map: |
| bugid_map[ rev.issue] = [] |
| bugid_map[ rev.issue].append( rev) |
| else: |
| other_revs.append( rev.revnum) |
| |
| # emit info about issues referenced in revisions |
| if len(bugid_map) and bzsoap: |
| htmlfile.write( "<h2>Issues addressed:</h2>\n<table border=\"0\" width=\"100%\">\n") |
| |
| soaprc = get_bug_details( bugid_map.keys()) |
| type2prio = {"FEATURE":1, "ENHANCEMENT":2, "PATCH":3, "DEFECT":4, "TASK":5, "UNKNOWN":9} |
| sorted_issues = sorted( soaprc["bugs"], |
| key = lambda b: type2prio[b["cf_bug_type"]]*1e9 + int(b["priority"][1:])*1e8 + int(b["id"])) |
| |
| type2color = { |
| "F1":"#0F0", "F2":"#0C0", "F3":"#080", "F4":"#040", "F5":"#020", |
| "E1":"#0C8", "E2":"#0A6", "E3":"#084", "E4":"#063", "E5":"#042", |
| "D1":"#F00", "D2":"#C00", "D3":"#800", "D4":"#600", "D5":"#300", |
| "P1":"#00F", "P2":"#00C", "P3":"#008", "P4":"#006", "P5":"#003", |
| "T1":"#0FF", "T2":"#0CC", "T3":"#088", "T4":"#066", "T5":"#063"}; |
| for bug in sorted_issues: |
| idnum = int( bug[ "id"]) |
| if bugref_url: |
| bug_url = bugref_url + str(idnum) |
| bug_desc = bug[ "summary"] |
| bug_type = bug[ "cf_bug_type"] |
| bug_target = bug[ "target_milestone"] |
| priority = bug[ "priority"] |
| if ("status" in bug): |
| bug_status = bug[ "status"] |
| if bug_status in ["RESOLVED","VERIFIED","CLOSED"]: |
| bug_status = bug[ "resolution"] |
| else: |
| bug_status = "UNKNOWN" |
| |
| colortype = bug_type[0]+priority[1] |
| if colortype in type2color: |
| color = type2color[ colortype] |
| else: |
| color = None |
| |
| idstr = ("#i%d#" if (detail_level >= 3) else "%d") % (idnum) |
| line = "<tr>" |
| if bug_url: |
| line += "<td><a href=\"%s\">%s</a></td>" % (bug_url, idstr) |
| else: |
| line += "<td>%s</td>" % (idstr) |
| if detail_level >= 5: |
| line += "<td>%s</td>" % (priority) |
| line += "<td>%s</td>" % (bug_type) |
| if detail_level >= 9: |
| line += "<td>" |
| for r in bugid_map[ idnum]: |
| revurl = svn_viewrev_url_base % (r.revnum) |
| revtitle = r.log.splitlines()[0] |
| line += "<a href=\"%s\" title=%s>c</a>" % (revurl, quoteattr(revtitle)) |
| line += "</td>" |
| if detail_level >= 7: |
| line += "<td>%s</td>" % (bug_target) |
| line += "<td>%s</td>" % (bug_status) |
| |
| line += "<td>" |
| if color: |
| line += "<font color=\"%s\">" % (color) |
| line += escape( bug_desc) |
| if color: |
| line += "</font>" |
| line += "<td>" |
| line += "</tr>\n" |
| htmlfile.write( line) |
| |
| htmlfile.write( "</table>\n") |
| |
| # emit info about other revisions |
| if (detail_level >= 6): |
| htmlfile.write( "<h2>Commits without Issue References:</h2>\n<table border=\"0\">\n") |
| for rev in all_revs: |
| if rev.issue: |
| if rev.issue not in soaprc["faults"]: |
| continue |
| |
| line = "<tr>" |
| if svn_viewrev_url_base: |
| revurl = svn_viewrev_url_base % (rev.revnum) |
| line += "<td><a href=\"%s\">r%d</a></td>" % (revurl, rev.revnum) |
| else: |
| line += "<td>r%d</td>" % (rev.revnum) |
| |
| summary = rev.log.splitlines()[0] |
| line += "<td>%s</td>" % (escape(summary)) |
| line += "</tr>\n" |
| htmlfile.write( line) |
| |
| htmlfile.write( "</table>\n") |
| |
| # emit html footer to the info file |
| htmlfile.write( "</body></html>\n") |
| |
| # print summary of the HTML file created |
| print "Processed %d revisions" % (len(all_revs)) |
| print "Found %d issues referenced" % (len(bugid_map)) |
| print "Wrote HTML file \"%s\"" % (htmlname) |
| |
| |
| def main(args): |
| if (len(args) < 4) or (5 < len(args)): |
| print "Usage: " + args[0] + " [svnurl|branchname] minrev maxrev [enduser|developer]" |
| sys.exit(1) |
| svnurl = args[1] |
| revmin = args[2] |
| revmax = args[3] |
| |
| if len(args) >= 5: |
| audience = args[4] |
| else: |
| audience = "developer" |
| |
| audience2verbosity = {"enduser":1, "developer":9} |
| if audience not in audience2verbosity: |
| print "Audience \"%s\" not known! Only \"%s\" can be selected." % (audience,str(audience2verbosity.keys())) |
| sys.exit(2) |
| detail_level = audience2verbosity[ audience] |
| |
| full_url_re = re.compile( "https?://") |
| if not full_url_re.match( svnurl): |
| svnurl = svn_default_root_url + svnurl |
| |
| svnout = get_svn_log( svnurl, revmin, revmax) |
| revlist = parse_svn_log_xml( svnout) |
| revs2info( infoout_name, detail_level, revlist, svnurl, revmin, revmax) |
| |
| |
| if __name__ == "__main__": |
| main(sys.argv[0:]) |
| |