scripts/svnlog2info.py (188 lines of code) (raw):
#! /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:])