mirror of
https://github.com/flatcar/scripts.git
synced 2025-09-24 15:11:19 +02:00
Most of this is from this code review: http://codereview.chromium.org/4175007/diff/1/2 I also ran the code through gpylint and fixed some of the stuff, like making quoting consistent. Change-Id: Icb3896dbd4e975c7ea4687d58efd6372adfcc3c9 BUG=chromium-os:8205 TEST=Ran ./cros_changelog 0.7.48.0 and saw error; also ran ./cros_changelog 0.9.98.3 Review URL: http://codereview.chromium.org/4263001
367 lines
12 KiB
Python
Executable File
367 lines
12 KiB
Python
Executable File
#!/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()
|