Index: chromite/bin/cros_changelog |
diff --git a/chromite/bin/cros_changelog b/chromite/bin/cros_changelog |
index c79c2e8c561b91e8309c0cab1a5285a7782c7cb2..73b54354515b852e01d72ec70cc78443ed6ef754 100755 |
--- a/chromite/bin/cros_changelog |
+++ b/chromite/bin/cros_changelog |
@@ -17,6 +17,30 @@ 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' |
@@ -38,11 +62,73 @@ def _GrabDirs(): |
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): |
+ body, tracker_acc): |
"""Create commit logs.""" |
self.commit = commit |
self.projectname = projectname |
@@ -51,10 +137,18 @@ class Commit(object): |
self.commit_date = datetime.strptime(commit_date, fmt) |
self.subject = subject |
self.body = body |
- self.bug_ids = self._GetBugIDs() |
+ self._tracker_acc = tracker_acc |
+ self._issues = self._GetIssues() |
+ |
+ def _GetIssues(self): |
+ """Get bug info from commit logs and issue tracker. |
- def _GetBugIDs(self): |
- """Get bug ID from commit logs.""" |
+ 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. |
+ """ |
entries = [] |
for line in self.body.split('\n'): |
@@ -63,7 +157,7 @@ class Commit(object): |
for i in match.group(1).split(','): |
entries.extend(filter(None, [x.strip() for x in i.split()])) |
- bug_ids = [] |
+ 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)') |
@@ -72,38 +166,32 @@ class Commit(object): |
bug_numbers = re.findall(regex, new_item) |
for bug_tuple in bug_numbers: |
if bug_tuple[0] and bug_tuple[1]: |
- bug_ids.append('%s:%s' % (bug_tuple[0], 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]: |
- bug_ids.append('%s:%s' % (bug_tuple[2], bug_tuple[3])) |
+ issues.append(Issue(bug_tuple[2], bug_tuple[3], self._tracker_acc)) |
last_tracker = bug_tuple[2] |
elif bug_tuple[4]: |
- bug_ids.append('%s:%s' % (last_tracker, bug_tuple[4])) |
+ issues.append(Issue(last_tracker, bug_tuple[4], self._tracker_acc)) |
- bug_ids.sort(key=str.lower) |
- return bug_ids |
+ 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 = [] |
- bug_url_fmt = 'http://code.google.com/p/%s/issues/detail?id=%s' |
link_fmt = '<a href="%s">%s</a>' |
- for bug in self.bug_ids: |
- tracker, bug_id = bug.split(':') |
- |
- # Get bug URL. We use short URLs to make the URLs a bit more readable. |
- if tracker == 'chromium-os': |
- bug_url = 'http://crosbug.com/%s' % bug_id |
- elif tracker == 'chrome-os-partner': |
- bug_url = 'http://crosbug.com/p/%s' % bug_id |
- else: |
- bug_url = bug_url_fmt % (tracker, bug_id) |
- |
- bugs.append(link_fmt % (bug_url, bug)) |
+ 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) |
@@ -132,7 +220,7 @@ class Commit(object): |
cmp(self.commit_date, other.commit_date)) |
-def _GrabChanges(path, tag1, tag2): |
+def _GrabChanges(path, tag1, tag2, tracker_acc): |
"""Return list of commits to path between tag1 and tag2.""" |
cmd = 'cd %s && git config --get remote.cros.projectname' % path |
@@ -145,14 +233,25 @@ def _GrabChanges(path, tag1, tag2): |
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) |
+ body, tracker_acc) |
commits.append(change) |
return commits |
+ |
def _ParseArgs(): |
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( |
+ "--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() |
@@ -178,11 +277,27 @@ def main(): |
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)) |
+ changes.extend(_GrabChanges(path, tag1, tag2, tracker_acc)) |
title = 'Changelog for %s to %s' % (tag1, tag2) |
print '<html>' |