Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(127)

Side by Side Diff: chromite/bin/cros_changelog

Issue 4102013: Added option for including priority/milestone to cros_changelog. (Closed) Base URL: ssh://git@gitrw.chromium.org:9222/crosutils.git
Patch Set: Fixes from David James code review. Created 10 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | chromite/lib/tracker_access.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/python 1 #!/usr/bin/python
2 2
3 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved. 3 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
4 # Use of this source code is governed by a BSD-style license that can be 4 # Use of this source code is governed by a BSD-style license that can be
5 # found in the LICENSE file. 5 # found in the LICENSE file.
6 6
7 """Helper script for printing differences between tags.""" 7 """Helper script for printing differences between tags."""
8 8
9 import cgi 9 import cgi
10 from datetime import datetime 10 from datetime import datetime
11 import operator 11 import operator
12 import optparse 12 import optparse
13 import os 13 import os
14 import re 14 import re
15 import sys 15 import sys
16 16
17 sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../lib')) 17 sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../lib'))
18 from cros_build_lib import RunCommand 18 from cros_build_lib import RunCommand
19 19
20
21 # TODO(dianders):
22 # We use GData to access the tracker on code.google.com. Eventually, we
23 # want to create an ebuild and add the ebuild to hard-host-depends
24 # For now, we'll just include instructions for installing it.
25 INSTRS_FOR_GDATA = """
26 To access the tracker you need the GData library. To install in your home dir:
27
28 GDATA_INSTALL_DIR=~/gdatalib
29 mkdir -p "$GDATA_INSTALL_DIR"
30
31 TMP_DIR=`mktemp -d`
32 pushd $TMP_DIR
33 wget http://gdata-python-client.googlecode.com/files/gdata-2.0.12.zip
34 unzip gdata-2.0.12.zip
35 cd gdata-2.0.12/
36 python setup.py install --home="$GDATA_INSTALL_DIR"
37 popd
38
39 export PYTHONPATH="$GDATA_INSTALL_DIR/lib/python:$PYTHONPATH"
40
41 You should add the PYTHONPATH line to your .bashrc file (or equivalent)."""
42
43
20 DEFAULT_TRACKER = 'chromium-os' 44 DEFAULT_TRACKER = 'chromium-os'
21 45
22 46
23 def _GrabOutput(cmd): 47 def _GrabOutput(cmd):
24 """Returns output from specified command.""" 48 """Returns output from specified command."""
25 return RunCommand(cmd, shell=True, print_cmd=False, 49 return RunCommand(cmd, shell=True, print_cmd=False,
26 redirect_stdout=True).output 50 redirect_stdout=True).output
27 51
28 52
29 def _GrabTags(): 53 def _GrabTags():
30 """Returns list of tags from current git repository.""" 54 """Returns list of tags from current git repository."""
31 cmd = ("git for-each-ref refs/tags | awk '{print $3}' | " 55 cmd = ("git for-each-ref refs/tags | awk '{print $3}' | "
32 "sed 's,refs/tags/,,g' | sort -t. -k3,3rn -k4,4rn") 56 "sed 's,refs/tags/,,g' | sort -t. -k3,3rn -k4,4rn")
33 return _GrabOutput(cmd).split() 57 return _GrabOutput(cmd).split()
34 58
35 59
36 def _GrabDirs(): 60 def _GrabDirs():
37 """Returns list of directories managed by repo.""" 61 """Returns list of directories managed by repo."""
38 return _GrabOutput('repo forall -c "pwd"').split() 62 return _GrabOutput('repo forall -c "pwd"').split()
39 63
40 64
65 class Issue(object):
66 """Class for holding info about issues (aka bugs)."""
67
68 def __init__(self, project_name, issue_id, tracker_acc):
69 """Constructor for Issue object.
70
71 Args:
72 project_name: The tracker project to query.
73 issue_id: The ID of the issue to query
74 tracker_acc: A TrackerAccess object, or None.
75 """
76 self.project_name = project_name
77 self.issue_id = issue_id
78 self.milestone = ''
79 self.priority = ''
80
81 if tracker_acc is not None:
82 keyed_labels = tracker_acc.GetKeyedLabels(project_name, issue_id)
83 if 'Mstone' in keyed_labels:
84 self.milestone = keyed_labels['Mstone']
85 if 'Pri' in keyed_labels:
86 self.priority = keyed_labels['Pri']
87
88 def GetUrl(self):
89 """Returns the URL to access the issue."""
90 bug_url_fmt = 'http://code.google.com/p/%s/issues/detail?id=%s'
91
92 # Get bug URL. We use short URLs to make the URLs a bit more readable.
93 if self.project_name == 'chromium-os':
94 bug_url = 'http://crosbug.com/%s' % self.issue_id
95 elif self.project_name == 'chrome-os-partner':
96 bug_url = 'http://crosbug.com/p/%s' % self.issue_id
97 else:
98 bug_url = bug_url_fmt % (self.project_name, self.issue_id)
99
100 return bug_url
101
102 def __str__(self):
103 """Provides a string representation of the issue.
104
105 Returns:
106 A string that looks something like:
107
108 project:id (milestone, priority)
109 """
110 if self.milestone and self.priority:
111 info_str = ' (%s, P%s)' % (self.milestone, self.priority)
112 elif self.milestone:
113 info_str = ' (%s)' % self.milestone
114 elif self.priority:
115 info_str = ' (P%s)' % self.priority
116 else:
117 info_str = ''
118
119 return '%s:%s%s' % (self.project_name, self.issue_id, info_str)
120
121 def __cmp__(self, other):
122 """Compare two Issue objects."""
123 return cmp((self.project_name.lower(), self.issue_id),
124 (other.project_name.lower(), other.issue_id))
125
126
41 class Commit(object): 127 class Commit(object):
42 """Class for tracking git commits.""" 128 """Class for tracking git commits."""
43 129
44 def __init__(self, commit, projectname, commit_email, commit_date, subject, 130 def __init__(self, commit, projectname, commit_email, commit_date, subject,
45 body): 131 body, tracker_acc):
46 """Create commit logs.""" 132 """Create commit logs."""
47 self.commit = commit 133 self.commit = commit
48 self.projectname = projectname 134 self.projectname = projectname
49 self.commit_email = commit_email 135 self.commit_email = commit_email
50 fmt = '%a %b %d %H:%M:%S %Y' 136 fmt = '%a %b %d %H:%M:%S %Y'
51 self.commit_date = datetime.strptime(commit_date, fmt) 137 self.commit_date = datetime.strptime(commit_date, fmt)
52 self.subject = subject 138 self.subject = subject
53 self.body = body 139 self.body = body
54 self.bug_ids = self._GetBugIDs() 140 self._tracker_acc = tracker_acc
141 self._issues = self._GetIssues()
55 142
56 def _GetBugIDs(self): 143 def _GetIssues(self):
57 """Get bug ID from commit logs.""" 144 """Get bug info from commit logs and issue tracker.
145
146 This should be called as the last step of __init__, since it
147 assumes that our member variables are already setup.
148
149 Returns:
150 A list of Issue objects, each of which holds info about a bug.
151 """
58 152
59 entries = [] 153 entries = []
60 for line in self.body.split('\n'): 154 for line in self.body.split('\n'):
61 match = re.match(r'^ *BUG *=(.*)', line) 155 match = re.match(r'^ *BUG *=(.*)', line)
62 if match: 156 if match:
63 for i in match.group(1).split(','): 157 for i in match.group(1).split(','):
64 entries.extend(filter(None, [x.strip() for x in i.split()])) 158 entries.extend(filter(None, [x.strip() for x in i.split()]))
65 159
66 bug_ids = [] 160 issues = []
67 last_tracker = DEFAULT_TRACKER 161 last_tracker = DEFAULT_TRACKER
68 regex = (r'http://code.google.com/p/(\S+)/issues/detail\?id=([0-9]+)' 162 regex = (r'http://code.google.com/p/(\S+)/issues/detail\?id=([0-9]+)'
69 r'|(\S+):([0-9]+)|(\b[0-9]+\b)') 163 r'|(\S+):([0-9]+)|(\b[0-9]+\b)')
70 164
71 for new_item in entries: 165 for new_item in entries:
72 bug_numbers = re.findall(regex, new_item) 166 bug_numbers = re.findall(regex, new_item)
73 for bug_tuple in bug_numbers: 167 for bug_tuple in bug_numbers:
74 if bug_tuple[0] and bug_tuple[1]: 168 if bug_tuple[0] and bug_tuple[1]:
75 bug_ids.append('%s:%s' % (bug_tuple[0], bug_tuple[1])) 169 issues.append(Issue(bug_tuple[0], bug_tuple[1], self._tracker_acc))
76 last_tracker = bug_tuple[0] 170 last_tracker = bug_tuple[0]
77 elif bug_tuple[2] and bug_tuple[3]: 171 elif bug_tuple[2] and bug_tuple[3]:
78 bug_ids.append('%s:%s' % (bug_tuple[2], bug_tuple[3])) 172 issues.append(Issue(bug_tuple[2], bug_tuple[3], self._tracker_acc))
79 last_tracker = bug_tuple[2] 173 last_tracker = bug_tuple[2]
80 elif bug_tuple[4]: 174 elif bug_tuple[4]:
81 bug_ids.append('%s:%s' % (last_tracker, bug_tuple[4])) 175 issues.append(Issue(last_tracker, bug_tuple[4], self._tracker_acc))
82 176
83 bug_ids.sort(key=str.lower) 177 issues.sort()
84 return bug_ids 178 return issues
85 179
86 def AsHTMLTableRow(self): 180 def AsHTMLTableRow(self):
87 """Returns HTML for this change, for printing as part of a table. 181 """Returns HTML for this change, for printing as part of a table.
88 182
89 Columns: Project, Date, Commit, Committer, Bugs, Subject. 183 Columns: Project, Date, Commit, Committer, Bugs, Subject.
184
185 Returns:
186 A string usable as an HTML table row, like:
187
188 <tr><td>Blah</td><td>Blah blah</td></tr>
90 """ 189 """
91 190
92 bugs = [] 191 bugs = []
93 bug_url_fmt = 'http://code.google.com/p/%s/issues/detail?id=%s'
94 link_fmt = '<a href="%s">%s</a>' 192 link_fmt = '<a href="%s">%s</a>'
95 for bug in self.bug_ids: 193 for issue in self._issues:
96 tracker, bug_id = bug.split(':') 194 bugs.append(link_fmt % (issue.GetUrl(), str(issue)))
97
98 # Get bug URL. We use short URLs to make the URLs a bit more readable.
99 if tracker == 'chromium-os':
100 bug_url = 'http://crosbug.com/%s' % bug_id
101 elif tracker == 'chrome-os-partner':
102 bug_url = 'http://crosbug.com/p/%s' % bug_id
103 else:
104 bug_url = bug_url_fmt % (tracker, bug_id)
105
106 bugs.append(link_fmt % (bug_url, bug))
107 195
108 url_fmt = 'http://chromiumos-git/git/?p=%s.git;a=commitdiff;h=%s' 196 url_fmt = 'http://chromiumos-git/git/?p=%s.git;a=commitdiff;h=%s'
109 url = url_fmt % (self.projectname, self.commit) 197 url = url_fmt % (self.projectname, self.commit)
110 commit_desc = link_fmt % (url, self.commit[:8]) 198 commit_desc = link_fmt % (url, self.commit[:8])
111 bug_str = '<br>'.join(bugs) 199 bug_str = '<br>'.join(bugs)
112 if not bug_str: 200 if not bug_str:
113 if (self.projectname == 'kernel-next' or 201 if (self.projectname == 'kernel-next' or
114 self.commit_email == 'chrome-bot@chromium.org'): 202 self.commit_email == 'chrome-bot@chromium.org'):
115 bug_str = 'not needed' 203 bug_str = 'not needed'
116 else: 204 else:
117 bug_str = '<font color="red">none</font>' 205 bug_str = '<font color="red">none</font>'
118 206
119 cols = [ 207 cols = [
120 cgi.escape(self.projectname), 208 cgi.escape(self.projectname),
121 str(self.commit_date), 209 str(self.commit_date),
122 commit_desc, 210 commit_desc,
123 cgi.escape(self.commit_email), 211 cgi.escape(self.commit_email),
124 bug_str, 212 bug_str,
125 cgi.escape(self.subject[:100]), 213 cgi.escape(self.subject[:100]),
126 ] 214 ]
127 return '<tr><td>%s</td></tr>' % ('</td><td>'.join(cols)) 215 return '<tr><td>%s</td></tr>' % ('</td><td>'.join(cols))
128 216
129 def __cmp__(self, other): 217 def __cmp__(self, other):
130 """Compare two Commit objects first by project name, then by date.""" 218 """Compare two Commit objects first by project name, then by date."""
131 return (cmp(self.projectname, other.projectname) or 219 return (cmp(self.projectname, other.projectname) or
132 cmp(self.commit_date, other.commit_date)) 220 cmp(self.commit_date, other.commit_date))
133 221
134 222
135 def _GrabChanges(path, tag1, tag2): 223 def _GrabChanges(path, tag1, tag2, tracker_acc):
136 """Return list of commits to path between tag1 and tag2.""" 224 """Return list of commits to path between tag1 and tag2."""
137 225
138 cmd = 'cd %s && git config --get remote.cros.projectname' % path 226 cmd = 'cd %s && git config --get remote.cros.projectname' % path
139 projectname = _GrabOutput(cmd).strip() 227 projectname = _GrabOutput(cmd).strip()
140 log_fmt = '%x00%H\t%ce\t%cd\t%s\t%b' 228 log_fmt = '%x00%H\t%ce\t%cd\t%s\t%b'
141 cmd_fmt = 'cd %s && git log --format="%s" --date=local "%s..%s"' 229 cmd_fmt = 'cd %s && git log --format="%s" --date=local "%s..%s"'
142 cmd = cmd_fmt % (path, log_fmt, tag1, tag2) 230 cmd = cmd_fmt % (path, log_fmt, tag1, tag2)
143 output = _GrabOutput(cmd) 231 output = _GrabOutput(cmd)
144 commits = [] 232 commits = []
145 for log_data in output.split('\0')[1:]: 233 for log_data in output.split('\0')[1:]:
146 commit, commit_email, commit_date, subject, body = log_data.split('\t', 4) 234 commit, commit_email, commit_date, subject, body = log_data.split('\t', 4)
147 change = Commit(commit, projectname, commit_email, commit_date, subject, 235 change = Commit(commit, projectname, commit_email, commit_date, subject,
148 body) 236 body, tracker_acc)
149 commits.append(change) 237 commits.append(change)
150 return commits 238 return commits
151 239
240
152 def _ParseArgs(): 241 def _ParseArgs():
153 parser = optparse.OptionParser() 242 parser = optparse.OptionParser()
154 parser.add_option("--sort-by-date", dest="sort_by_date", default=False, 243 parser.add_option(
155 action='store_true', help="Sort commits by date.") 244 "--sort-by-date", dest="sort_by_date", default=False,
245 action='store_true', help="Sort commits by date.")
246 parser.add_option(
247 "--tracker-user", dest="tracker_user", default=None,
248 help="Specify a username to login to code.google.com.")
249 parser.add_option(
250 "--tracker-pass", dest="tracker_pass", default=None,
251 help="Specify a password to go w/ user.")
252 parser.add_option(
253 "--tracker-passfile", dest="tracker_passfile", default=None,
254 help="Specify a file containing a password to go w/ user.")
156 return parser.parse_args() 255 return parser.parse_args()
157 256
158 257
159 def main(): 258 def main():
160 tags = _GrabTags() 259 tags = _GrabTags()
161 tag1 = None 260 tag1 = None
162 options, args = _ParseArgs() 261 options, args = _ParseArgs()
163 if len(args) == 2: 262 if len(args) == 2:
164 tag1, tag2 = args 263 tag1, tag2 = args
165 elif len(args) == 1: 264 elif len(args) == 1:
166 tag2, = args 265 tag2, = args
167 if tag2 in tags: 266 if tag2 in tags:
168 tag1 = tags[tags.index(tag2) + 1] 267 tag1 = tags[tags.index(tag2) + 1]
169 else: 268 else:
170 print >>sys.stderr, 'Unrecognized tag: %s' % tag2 269 print >>sys.stderr, 'Unrecognized tag: %s' % tag2
171 sys.exit(1) 270 sys.exit(1)
172 else: 271 else:
173 print >>sys.stderr, 'Usage: %s [tag1] tag2' % sys.argv[0] 272 print >>sys.stderr, 'Usage: %s [tag1] tag2' % sys.argv[0]
174 print >>sys.stderr, 'If only one tag is specified, we view the differences' 273 print >>sys.stderr, 'If only one tag is specified, we view the differences'
175 print >>sys.stderr, 'between that tag and the previous tag. You can also' 274 print >>sys.stderr, 'between that tag and the previous tag. You can also'
176 print >>sys.stderr, 'specify cros/master to show differences with' 275 print >>sys.stderr, 'specify cros/master to show differences with'
177 print >>sys.stderr, 'tip-of-tree.' 276 print >>sys.stderr, 'tip-of-tree.'
178 print >>sys.stderr, 'E.g. %s %s cros/master' % (sys.argv[0], tags[0]) 277 print >>sys.stderr, 'E.g. %s %s cros/master' % (sys.argv[0], tags[0])
179 sys.exit(1) 278 sys.exit(1)
180 279
280 if options.tracker_user is not None:
281 # TODO(dianders): Once we install GData automatically, move the import
282 # to the top of the file where it belongs. It's only here to allow
283 # people to run the script without GData.
284 try:
285 import tracker_access
286 except ImportError:
287 print >>sys.stderr, INSTRS_FOR_GDATA
288 sys.exit(1)
289 if options.tracker_passfile is not None:
290 options.tracker_pass = open(options.tracker_passfile, "r").read().strip()
291 tracker_acc = tracker_access.TrackerAccess(options.tracker_user,
292 options.tracker_pass)
293 else:
294 tracker_acc = None
295
181 print >>sys.stderr, 'Finding differences between %s and %s' % (tag1, tag2) 296 print >>sys.stderr, 'Finding differences between %s and %s' % (tag1, tag2)
182 paths = _GrabDirs() 297 paths = _GrabDirs()
183 changes = [] 298 changes = []
184 for path in paths: 299 for path in paths:
185 changes.extend(_GrabChanges(path, tag1, tag2)) 300 changes.extend(_GrabChanges(path, tag1, tag2, tracker_acc))
186 301
187 title = 'Changelog for %s to %s' % (tag1, tag2) 302 title = 'Changelog for %s to %s' % (tag1, tag2)
188 print '<html>' 303 print '<html>'
189 print '<head><title>%s</title></head>' % title 304 print '<head><title>%s</title></head>' % title
190 print '<h1>%s</h1>' % title 305 print '<h1>%s</h1>' % title
191 cols = ['Project', 'Date', 'Commit', 'Committer', 'Bugs', 'Subject'] 306 cols = ['Project', 'Date', 'Commit', 'Committer', 'Bugs', 'Subject']
192 print '<table border="1" cellpadding="4">' 307 print '<table border="1" cellpadding="4">'
193 print '<tr><th>%s</th>' % ('</th><th>'.join(cols)) 308 print '<tr><th>%s</th>' % ('</th><th>'.join(cols))
194 if options.sort_by_date: 309 if options.sort_by_date:
195 changes.sort(key=operator.attrgetter("commit_date")) 310 changes.sort(key=operator.attrgetter("commit_date"))
196 else: 311 else:
197 changes.sort() 312 changes.sort()
198 for change in changes: 313 for change in changes:
199 print change.AsHTMLTableRow() 314 print change.AsHTMLTableRow()
200 print '</table>' 315 print '</table>'
201 print '</html>' 316 print '</html>'
202 317
203 318
204 if __name__ == '__main__': 319 if __name__ == '__main__':
205 main() 320 main()
OLDNEW
« no previous file with comments | « no previous file | chromite/lib/tracker_access.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698