mirror of
https://github.com/flatcar/scripts.git
synced 2025-08-09 14:06: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'))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../lib'))
|
||||||
from cros_build_lib import RunCommand
|
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'
|
DEFAULT_TRACKER = 'chromium-os'
|
||||||
|
|
||||||
|
|
||||||
@ -38,11 +62,73 @@ def _GrabDirs():
|
|||||||
return _GrabOutput('repo forall -c "pwd"').split()
|
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 Commit(object):
|
||||||
"""Class for tracking git commits."""
|
"""Class for tracking git commits."""
|
||||||
|
|
||||||
def __init__(self, commit, projectname, commit_email, commit_date, subject,
|
def __init__(self, commit, projectname, commit_email, commit_date, subject,
|
||||||
body):
|
body, tracker_acc):
|
||||||
"""Create commit logs."""
|
"""Create commit logs."""
|
||||||
self.commit = commit
|
self.commit = commit
|
||||||
self.projectname = projectname
|
self.projectname = projectname
|
||||||
@ -51,10 +137,18 @@ class Commit(object):
|
|||||||
self.commit_date = datetime.strptime(commit_date, fmt)
|
self.commit_date = datetime.strptime(commit_date, fmt)
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
self.body = body
|
self.body = body
|
||||||
self.bug_ids = self._GetBugIDs()
|
self._tracker_acc = tracker_acc
|
||||||
|
self._issues = self._GetIssues()
|
||||||
|
|
||||||
def _GetBugIDs(self):
|
def _GetIssues(self):
|
||||||
"""Get bug ID from commit logs."""
|
"""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 = []
|
entries = []
|
||||||
for line in self.body.split('\n'):
|
for line in self.body.split('\n'):
|
||||||
@ -63,7 +157,7 @@ class Commit(object):
|
|||||||
for i in match.group(1).split(','):
|
for i in match.group(1).split(','):
|
||||||
entries.extend(filter(None, [x.strip() for x in i.split()]))
|
entries.extend(filter(None, [x.strip() for x in i.split()]))
|
||||||
|
|
||||||
bug_ids = []
|
issues = []
|
||||||
last_tracker = DEFAULT_TRACKER
|
last_tracker = DEFAULT_TRACKER
|
||||||
regex = (r'http://code.google.com/p/(\S+)/issues/detail\?id=([0-9]+)'
|
regex = (r'http://code.google.com/p/(\S+)/issues/detail\?id=([0-9]+)'
|
||||||
r'|(\S+):([0-9]+)|(\b[0-9]+\b)')
|
r'|(\S+):([0-9]+)|(\b[0-9]+\b)')
|
||||||
@ -72,38 +166,32 @@ class Commit(object):
|
|||||||
bug_numbers = re.findall(regex, new_item)
|
bug_numbers = re.findall(regex, new_item)
|
||||||
for bug_tuple in bug_numbers:
|
for bug_tuple in bug_numbers:
|
||||||
if bug_tuple[0] and bug_tuple[1]:
|
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]
|
last_tracker = bug_tuple[0]
|
||||||
elif bug_tuple[2] and bug_tuple[3]:
|
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]
|
last_tracker = bug_tuple[2]
|
||||||
elif bug_tuple[4]:
|
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)
|
issues.sort()
|
||||||
return bug_ids
|
return issues
|
||||||
|
|
||||||
def AsHTMLTableRow(self):
|
def AsHTMLTableRow(self):
|
||||||
"""Returns HTML for this change, for printing as part of a table.
|
"""Returns HTML for this change, for printing as part of a table.
|
||||||
|
|
||||||
Columns: Project, Date, Commit, Committer, Bugs, Subject.
|
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 = []
|
bugs = []
|
||||||
bug_url_fmt = 'http://code.google.com/p/%s/issues/detail?id=%s'
|
|
||||||
link_fmt = '<a href="%s">%s</a>'
|
link_fmt = '<a href="%s">%s</a>'
|
||||||
for bug in self.bug_ids:
|
for issue in self._issues:
|
||||||
tracker, bug_id = bug.split(':')
|
bugs.append(link_fmt % (issue.GetUrl(), str(issue)))
|
||||||
|
|
||||||
# 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))
|
|
||||||
|
|
||||||
url_fmt = 'http://chromiumos-git/git/?p=%s.git;a=commitdiff;h=%s'
|
url_fmt = 'http://chromiumos-git/git/?p=%s.git;a=commitdiff;h=%s'
|
||||||
url = url_fmt % (self.projectname, self.commit)
|
url = url_fmt % (self.projectname, self.commit)
|
||||||
@ -132,7 +220,7 @@ class Commit(object):
|
|||||||
cmp(self.commit_date, other.commit_date))
|
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."""
|
"""Return list of commits to path between tag1 and tag2."""
|
||||||
|
|
||||||
cmd = 'cd %s && git config --get remote.cros.projectname' % path
|
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:]:
|
for log_data in output.split('\0')[1:]:
|
||||||
commit, commit_email, commit_date, subject, body = log_data.split('\t', 4)
|
commit, commit_email, commit_date, subject, body = log_data.split('\t', 4)
|
||||||
change = Commit(commit, projectname, commit_email, commit_date, subject,
|
change = Commit(commit, projectname, commit_email, commit_date, subject,
|
||||||
body)
|
body, tracker_acc)
|
||||||
commits.append(change)
|
commits.append(change)
|
||||||
return commits
|
return commits
|
||||||
|
|
||||||
|
|
||||||
def _ParseArgs():
|
def _ParseArgs():
|
||||||
parser = optparse.OptionParser()
|
parser = optparse.OptionParser()
|
||||||
parser.add_option("--sort-by-date", dest="sort_by_date", default=False,
|
parser.add_option(
|
||||||
|
"--sort-by-date", dest="sort_by_date", default=False,
|
||||||
action='store_true', help="Sort commits by date.")
|
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()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
@ -178,11 +277,27 @@ def main():
|
|||||||
print >>sys.stderr, 'E.g. %s %s cros/master' % (sys.argv[0], tags[0])
|
print >>sys.stderr, 'E.g. %s %s cros/master' % (sys.argv[0], tags[0])
|
||||||
sys.exit(1)
|
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)
|
print >>sys.stderr, 'Finding differences between %s and %s' % (tag1, tag2)
|
||||||
paths = _GrabDirs()
|
paths = _GrabDirs()
|
||||||
changes = []
|
changes = []
|
||||||
for path in paths:
|
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)
|
title = 'Changelog for %s to %s' % (tag1, tag2)
|
||||||
print '<html>'
|
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