mirror of
https://github.com/flatcar/scripts.git
synced 2025-08-09 05:56:58 +02:00
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:
parent
142e452d37
commit
48849e2dea
@ -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>'
|
||||
|
166
chromite/lib/tracker_access.py
Normal file
166
chromite/lib/tracker_access.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user