Added option for including priority/milestone to cros_changelog.

You can access this by passing:
   --tracker-user="user@chromium.org"
   --tracker-passfile="fileContainingPassword"

...to access anonymously, just set --tracker-user=""

...if you don't include the --tracker-user option, we won't try to
fetch priority/milestone.

To use this feature, you need the GData library.  Until we get that
put in hard-host-depends, the script will simply print instructions
for installing GData if it detects that you don't have it.

At the moment, I believe that logging in isn't giving you any extra
access.  Therefore, any bugs that don't allow anonymous access will
not show their priority/milestone.  I am working on figuring out what
the problem is there.

Change-Id: If388c20c43ee2fb0c1ab8f748ffea65e354eeb1e

BUG=chromium-os:8205
TEST=Ran ./cros_changelog 0.9.104.0 --tracker-user="" and verified that some bugs got priority/milestone.

Review URL: http://codereview.chromium.org/4102013
This commit is contained in:
Doug Anderson 2010-10-29 14:50:15 -07:00
parent 142e452d37
commit 48849e2dea
2 changed files with 309 additions and 28 deletions

View File

@ -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 _GetBugIDs(self):
"""Get bug ID from commit logs."""
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.
"""
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>'

View File

@ -0,0 +1,166 @@
#!/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 functions for accessing the issue tracker in a pythonic way."""
import os.path
import pprint
import sys
# import the GData libraries
import gdata.client
import gdata.projecthosting.client
DEFAULT_TRACKER_SOURCE = "chromite-tracker-access-1.0"
VERBOSE = True # Set to True to get extra debug info...
class TrackerAccess(object):
"""Class for accessing the tracker on code.google.com."""
def __init__(self, email="", password="",
tracker_source=DEFAULT_TRACKER_SOURCE):
"""TrackerAccess constructor.
Args:
email: The email address to Login with; may be "" for anonymous access.
password: The password that goes with the email address; may be "" if
the email is "".
tracker_source: A string describing this program. This can be anything
you like but should should give some indication of which
app is making the request.
"""
# Save parameters...
self._email = email
self._password = password
self._tracker_source = tracker_source
# This will be initted on first login...
self._tracker_client = None
def Login(self):
"""Login, if needed. This may be safely called more than once.
Commands will call this function as their first line, so the client
of this class need not call it themselves unless trying to debug login
problems.
This function should be called even if we're accessing anonymously.
"""
# Bail immediately if we've already logged in...
if self._tracker_client is not None:
return
self._tracker_client = gdata.projecthosting.client.ProjectHostingClient()
if self._email and self._password:
self._tracker_client.client_login(self._email, self._password,
source=self._tracker_source,
service="code", account_type='GOOGLE')
def GetKeyedLabels(self, project_name, issue_id):
"""Get labels of the form "Key-Value" attached to the given issue.
Any labels that don't have a dash in them are ignored.
Args:
project_name: The tracker project to query.
issue_id: The ID of the issue to query; should be an int but a string
will probably work too.
Returns:
A dictionary mapping key/value pairs from the issue's labels, like:
{'Area': 'Build',
'Iteration': '15',
'Mstone': 'R9.x',
'Pri': '1',
'Type': 'Bug'}
"""
# Login if needed...
self.Login()
# Construct the query...
query = gdata.projecthosting.client.Query(issue_id=issue_id)
try:
feed = self._tracker_client.get_issues(project_name, query=query)
except gdata.client.RequestError, e:
if VERBOSE:
print >>sys.stderr, "ERROR: Unable to access bug %s:%s: %s" % (
project_name, issue_id, str(e))
return {}
# There should be exactly one result...
assert len(feed.entry) == 1, "Expected exactly 1 result"
(entry,) = feed.entry
# We only care about labels that look like: Key-Value
# We'll return a dictionary of those.
keyed_labels = {}
for label in entry.label:
if "-" in label.text:
label_key, label_val = label.text.split("-", 1)
keyed_labels[label_key] = label_val
return keyed_labels
def _TestGetKeyedLabels(project_name, email, passwordFile, *bug_ids):
"""Test code for GetKeyedLabels().
Args:
project_name: The name of the project we're looking at.
email: The email address to use to login. May be ""
passwordFile: A file containing the password for the email address.
May be "" if email is "" for anon access.
bug_ids: A list of bug IDs to query.
"""
# If password was specified as a file, read it.
if passwordFile:
password = open(passwordFile, "r").read().strip()
else:
password = ""
ta = TrackerAccess(email, password)
if not bug_ids:
print "No bugs were specified"
else:
for bug_id in bug_ids:
print bug_id, ta.GetKeyedLabels(project_name, int(bug_id))
def _DoHelp(commands, *args):
"""Print help for the script."""
if len(args) >= 2 and args[0] == "help" and args[1] in commands:
# If called with arguments 'help' and 'command', show that commands's doc.
command_name = args[1]
print commands[command_name].__doc__
else:
# Something else: show generic help...
print (
"Usage %s <command> <command args>\n"
"\n"
"Known commands: \n"
" %s\n"
) % (sys.argv[0], pprint.pformat(["help"] + sorted(commands)))
def main():
"""Main function of the script."""
commands = {
"TestGetKeyedLabels": _TestGetKeyedLabels,
}
if len(sys.argv) <= 1 or sys.argv[1] not in commands:
# Argument 1 isn't in list of commands; show help and pass all arguments...
_DoHelp(commands, *sys.argv[1:])
else:
command_name = sys.argv[1]
commands[command_name](*sys.argv[2:])
if __name__ == "__main__":
main()