blob: f75ceab6a830468dd5f607cd99f66dce9d5c7fa5 [file] [log] [blame]
#!/usr/bin/python
# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Helper script for printing differences between tags."""
import cgi
from datetime import datetime
import operator
import optparse
import os
import re
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../lib'))
from cros_build_lib import RunCommand
# TODO(dianders):
# We use GData to access the tracker on code.google.com. Eventually, we
# want to create an ebuild and add the ebuild to hard-host-depends
# For now, we'll just include instructions for installing it.
INSTRS_FOR_GDATA = """
To access the tracker you need the GData library. To install in your home dir:
GDATA_INSTALL_DIR=~/gdatalib
mkdir -p "$GDATA_INSTALL_DIR"
TMP_DIR=`mktemp -d`
pushd $TMP_DIR
wget http://gdata-python-client.googlecode.com/files/gdata-2.0.12.zip
unzip gdata-2.0.12.zip
cd gdata-2.0.12/
python setup.py install --home="$GDATA_INSTALL_DIR"
popd
export PYTHONPATH="$GDATA_INSTALL_DIR/lib/python:$PYTHONPATH"
You should add the PYTHONPATH line to your .bashrc file (or equivalent)."""
DEFAULT_TRACKER = 'chromium-os'
def _GrabOutput(cmd):
"""Returns output from specified command."""
return RunCommand(cmd, shell=True, print_cmd=False,
redirect_stdout=True).output
def _GrabTags():
"""Returns list of tags from current git repository."""
# TODO(dianders): replace this with the python equivalent.
cmd = ("git for-each-ref refs/tags | awk '{print $3}' | "
"sed 's,refs/tags/,,g' | sort -t. -k3,3rn -k4,4rn")
return _GrabOutput(cmd).split()
def _GrabDirs():
"""Returns list of directories managed by repo."""
return _GrabOutput('repo forall -c "pwd"').split()
class Issue(object):
"""Class for holding info about issues (aka bugs)."""
def __init__(self, project_name, issue_id, tracker_acc):
"""Constructor for Issue object.
Args:
project_name: The tracker project to query.
issue_id: The ID of the issue to query
tracker_acc: A TrackerAccess object, or None.
"""
self.project_name = project_name
self.issue_id = issue_id
self.milestone = ''
self.priority = ''
if tracker_acc is not None:
keyed_labels = tracker_acc.GetKeyedLabels(project_name, issue_id)
if 'Mstone' in keyed_labels:
self.milestone = keyed_labels['Mstone']
if 'Pri' in keyed_labels:
self.priority = keyed_labels['Pri']
def GetUrl(self):
"""Returns the URL to access the issue."""
bug_url_fmt = 'http://code.google.com/p/%s/issues/detail?id=%s'
# Get bug URL. We use short URLs to make the URLs a bit more readable.
if self.project_name == 'chromium-os':
bug_url = 'http://crosbug.com/%s' % self.issue_id
elif self.project_name == 'chrome-os-partner':
bug_url = 'http://crosbug.com/p/%s' % self.issue_id
else:
bug_url = bug_url_fmt % (self.project_name, self.issue_id)
return bug_url
def __str__(self):
"""Provides a string representation of the issue.
Returns:
A string that looks something like:
project:id (milestone, priority)
"""
if self.milestone and self.priority:
info_str = ' (%s, P%s)' % (self.milestone, self.priority)
elif self.milestone:
info_str = ' (%s)' % self.milestone
elif self.priority:
info_str = ' (P%s)' % self.priority
else:
info_str = ''
return '%s:%s%s' % (self.project_name, self.issue_id, info_str)
def __cmp__(self, other):
"""Compare two Issue objects."""
return cmp((self.project_name.lower(), self.issue_id),
(other.project_name.lower(), other.issue_id))
class Commit(object):
"""Class for tracking git commits."""
def __init__(self, commit, projectname, commit_email, commit_date, subject,
body, tracker_acc):
"""Create commit logs.
Args:
commit: The commit hash (sha) from git.
projectname: The project name, from:
git config --get remote.cros.projectname
commit_email: The email address associated with the commit (%ce in git
log)
commit_date: The date of the commit, like "Mon Nov 1 17:34:14 2010 -0500"
(%cd in git log))
subject: The subject of the commit (%s in git log)
body: The body of the commit (%b in git log)
tracker_acc: A tracker_access.TrackerAccess object.
"""
self.commit = commit
self.projectname = projectname
self.commit_email = commit_email
fmt = '%a %b %d %H:%M:%S %Y'
self.commit_date = datetime.strptime(commit_date, fmt)
self.subject = subject
self.body = body
self._tracker_acc = tracker_acc
self._issues = self._GetIssues()
def _GetIssues(self):
"""Get bug info from commit logs and issue tracker.
This should be called as the last step of __init__, since it
assumes that our member variables are already setup.
Returns:
A list of Issue objects, each of which holds info about a bug.
"""
# NOTE: most of this code is copied from bugdroid:
# <http://src.chromium.org/viewvc/chrome/trunk/tools/bugdroid/bugdroid.py?revision=59229&view=markup>
# Get a list of bugs. Handle lots of possibilities:
# - Multiple "BUG=" lines, with varying amounts of whitespace.
# - For each BUG= line, bugs can be split by commas _or_ by whitespace (!)
entries = []
for line in self.body.split('\n'):
match = re.match(r'^ *BUG *=(.*)', line)
if match:
for i in match.group(1).split(','):
entries.extend(filter(None, [x.strip() for x in i.split()]))
# Try to parse the bugs. Handle lots of different formats:
# - The whole URL, from which we parse the project and bug.
# - A simple string that looks like "project:bug"
# - A string that looks like "bug", which will always refer to the previous
# tracker referenced (defaulting to the default tracker).
#
# We will create an "Issue" object for each bug.
issues = []
last_tracker = DEFAULT_TRACKER
regex = (r'http://code.google.com/p/(\S+)/issues/detail\?id=([0-9]+)'
r'|(\S+):([0-9]+)|(\b[0-9]+\b)')
for new_item in entries:
bug_numbers = re.findall(regex, new_item)
for bug_tuple in bug_numbers:
if bug_tuple[0] and bug_tuple[1]:
issues.append(Issue(bug_tuple[0], bug_tuple[1], self._tracker_acc))
last_tracker = bug_tuple[0]
elif bug_tuple[2] and bug_tuple[3]:
issues.append(Issue(bug_tuple[2], bug_tuple[3], self._tracker_acc))
last_tracker = bug_tuple[2]
elif bug_tuple[4]:
issues.append(Issue(last_tracker, bug_tuple[4], self._tracker_acc))
# Sort the issues and return...
issues.sort()
return issues
def AsHTMLTableRow(self):
"""Returns HTML for this change, for printing as part of a table.
Columns: Project, Date, Commit, Committer, Bugs, Subject.
Returns:
A string usable as an HTML table row, like:
<tr><td>Blah</td><td>Blah blah</td></tr>
"""
bugs = []
link_fmt = '<a href="%s">%s</a>'
for issue in self._issues:
bugs.append(link_fmt % (issue.GetUrl(), str(issue)))
url_fmt = 'http://chromiumos-git/git/?p=%s.git;a=commitdiff;h=%s'
url = url_fmt % (self.projectname, self.commit)
commit_desc = link_fmt % (url, self.commit[:8])
bug_str = '<br>'.join(bugs)
if not bug_str:
if (self.projectname == 'kernel-next' or
self.commit_email == 'chrome-bot@chromium.org'):
bug_str = 'not needed'
else:
bug_str = '<font color="red">none</font>'
cols = [
cgi.escape(self.projectname),
str(self.commit_date),
commit_desc,
cgi.escape(self.commit_email),
bug_str,
cgi.escape(self.subject[:100]),
]
return '<tr><td>%s</td></tr>' % ('</td><td>'.join(cols))
def __cmp__(self, other):
"""Compare two Commit objects first by project name, then by date."""
return (cmp(self.projectname, other.projectname) or
cmp(self.commit_date, other.commit_date))
def _GrabChanges(path, tag1, tag2, tracker_acc):
"""Return list of commits to path between tag1 and tag2.
Args:
path: One of the directories managed by repo.
tag1: The first of the two tags to pass to git log.
tag2: The second of the two tags to pass to git log.
tracker_acc: A tracker_access.TrackerAccess object.
Returns:
A list of "Commit" objects.
"""
cmd = 'cd %s && git config --get remote.cros.projectname' % path
projectname = _GrabOutput(cmd).strip()
log_fmt = '%x00%H\t%ce\t%cd\t%s\t%b'
cmd_fmt = 'cd %s && git log --format="%s" --date=local "%s..%s"'
cmd = cmd_fmt % (path, log_fmt, tag1, tag2)
output = _GrabOutput(cmd)
commits = []
for log_data in output.split('\0')[1:]:
commit, commit_email, commit_date, subject, body = log_data.split('\t', 4)
change = Commit(commit, projectname, commit_email, commit_date, subject,
body, tracker_acc)
commits.append(change)
return commits
def _ParseArgs():
"""Parse command-line arguments.
Returns:
An optparse.OptionParser object.
"""
parser = optparse.OptionParser()
parser.add_option(
'--sort-by-date', dest='sort_by_date', default=False,
action='store_true', help='Sort commits by date.')
parser.add_option(
'--tracker-user', dest='tracker_user', default=None,
help='Specify a username to login to code.google.com.')
parser.add_option(
'--tracker-pass', dest='tracker_pass', default=None,
help='Specify a password to go w/ user.')
parser.add_option(
'--tracker-passfile', dest='tracker_passfile', default=None,
help='Specify a file containing a password to go w/ user.')
return parser.parse_args()
def main():
tags = _GrabTags()
tag1 = None
options, args = _ParseArgs()
if len(args) == 2:
tag1, tag2 = args
elif len(args) == 1:
tag2, = args
if tag2 in tags:
tag2_index = tags.index(tag2)
if tag2_index == len(tags) - 1:
print >>sys.stderr, 'No previous tag for %s' % tag2
sys.exit(1)
tag1 = tags[tag2_index + 1]
else:
print >>sys.stderr, 'Unrecognized tag: %s' % tag2
sys.exit(1)
else:
print >>sys.stderr, 'Usage: %s [tag1] tag2' % sys.argv[0]
print >>sys.stderr, 'If only one tag is specified, we view the differences'
print >>sys.stderr, 'between that tag and the previous tag. You can also'
print >>sys.stderr, 'specify cros/master to show differences with'
print >>sys.stderr, 'tip-of-tree.'
print >>sys.stderr, 'E.g. %s %s cros/master' % (sys.argv[0], tags[0])
sys.exit(1)
if options.tracker_user is not None:
# TODO(dianders): Once we install GData automatically, move the import
# to the top of the file where it belongs. It's only here to allow
# people to run the script without GData.
try:
import tracker_access
except ImportError:
print >>sys.stderr, INSTRS_FOR_GDATA
sys.exit(1)
if options.tracker_passfile is not None:
options.tracker_pass = open(options.tracker_passfile, 'r').read().strip()
tracker_acc = tracker_access.TrackerAccess(options.tracker_user,
options.tracker_pass)
else:
tracker_acc = None
print >>sys.stderr, 'Finding differences between %s and %s' % (tag1, tag2)
paths = _GrabDirs()
changes = []
for path in paths:
changes.extend(_GrabChanges(path, tag1, tag2, tracker_acc))
title = 'Changelog for %s to %s' % (tag1, tag2)
print '<html>'
print '<head><title>%s</title></head>' % title
print '<h1>%s</h1>' % title
cols = ['Project', 'Date', 'Commit', 'Committer', 'Bugs', 'Subject']
print '<table border="1" cellpadding="4">'
print '<tr><th>%s</th>' % ('</th><th>'.join(cols))
if options.sort_by_date:
changes.sort(key=operator.attrgetter('commit_date'))
else:
changes.sort()
for change in changes:
print change.AsHTMLTableRow()
print '</table>'
print '</html>'
if __name__ == '__main__':
main()