OLD | NEW |
---|---|
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 |
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
45 | 45 |
46 | 46 |
47 def _GrabOutput(cmd): | 47 def _GrabOutput(cmd): |
48 """Returns output from specified command.""" | 48 """Returns output from specified command.""" |
49 return RunCommand(cmd, shell=True, print_cmd=False, | 49 return RunCommand(cmd, shell=True, print_cmd=False, |
50 redirect_stdout=True).output | 50 redirect_stdout=True).output |
51 | 51 |
52 | 52 |
53 def _GrabTags(): | 53 def _GrabTags(): |
54 """Returns list of tags from current git repository.""" | 54 """Returns list of tags from current git repository.""" |
55 # TODO(dianders): replace this with the python equivalent. | |
55 cmd = ("git for-each-ref refs/tags | awk '{print $3}' | " | 56 cmd = ("git for-each-ref refs/tags | awk '{print $3}' | " |
56 "sed 's,refs/tags/,,g' | sort -t. -k3,3rn -k4,4rn") | 57 "sed 's,refs/tags/,,g' | sort -t. -k3,3rn -k4,4rn") |
57 return _GrabOutput(cmd).split() | 58 return _GrabOutput(cmd).split() |
58 | 59 |
59 | 60 |
60 def _GrabDirs(): | 61 def _GrabDirs(): |
61 """Returns list of directories managed by repo.""" | 62 """Returns list of directories managed by repo.""" |
62 return _GrabOutput('repo forall -c "pwd"').split() | 63 return _GrabOutput('repo forall -c "pwd"').split() |
63 | 64 |
64 | 65 |
(...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
122 """Compare two Issue objects.""" | 123 """Compare two Issue objects.""" |
123 return cmp((self.project_name.lower(), self.issue_id), | 124 return cmp((self.project_name.lower(), self.issue_id), |
124 (other.project_name.lower(), other.issue_id)) | 125 (other.project_name.lower(), other.issue_id)) |
125 | 126 |
126 | 127 |
127 class Commit(object): | 128 class Commit(object): |
128 """Class for tracking git commits.""" | 129 """Class for tracking git commits.""" |
129 | 130 |
130 def __init__(self, commit, projectname, commit_email, commit_date, subject, | 131 def __init__(self, commit, projectname, commit_email, commit_date, subject, |
131 body, tracker_acc): | 132 body, tracker_acc): |
132 """Create commit logs.""" | 133 """Create commit logs. |
134 | |
135 Args: | |
136 commit: The commit hash (sha) from git. | |
137 projectname: The project name, from: | |
138 git config --get remote.cros.projectname | |
139 commit_email: The email address associated with the commit (%ce in git | |
140 log) | |
141 commit_date: The date of the commit, like "Mon Nov 1 17:34:14 2010 -0500" | |
142 (%cd in git log)) | |
143 subject: The subject of the commit (%s in git log) | |
144 body: The body of the commit (%b in git log) | |
145 tracker_acc: A tracker_access.TrackerAccess object. | |
146 """ | |
133 self.commit = commit | 147 self.commit = commit |
134 self.projectname = projectname | 148 self.projectname = projectname |
135 self.commit_email = commit_email | 149 self.commit_email = commit_email |
136 fmt = '%a %b %d %H:%M:%S %Y' | 150 fmt = '%a %b %d %H:%M:%S %Y' |
137 self.commit_date = datetime.strptime(commit_date, fmt) | 151 self.commit_date = datetime.strptime(commit_date, fmt) |
138 self.subject = subject | 152 self.subject = subject |
139 self.body = body | 153 self.body = body |
140 self._tracker_acc = tracker_acc | 154 self._tracker_acc = tracker_acc |
141 self._issues = self._GetIssues() | 155 self._issues = self._GetIssues() |
142 | 156 |
143 def _GetIssues(self): | 157 def _GetIssues(self): |
144 """Get bug info from commit logs and issue tracker. | 158 """Get bug info from commit logs and issue tracker. |
145 | 159 |
146 This should be called as the last step of __init__, since it | 160 This should be called as the last step of __init__, since it |
147 assumes that our member variables are already setup. | 161 assumes that our member variables are already setup. |
148 | 162 |
149 Returns: | 163 Returns: |
150 A list of Issue objects, each of which holds info about a bug. | 164 A list of Issue objects, each of which holds info about a bug. |
151 """ | 165 """ |
166 # NOTE: most of this code is copied from bugdroid: | |
167 # <http://src.chromium.org/viewvc/chrome/trunk/tools/bugdroid/bugdroid.py? revision=59229&view=markup> | |
152 | 168 |
169 # Get a list of bugs. Handle lots of possibilities: | |
170 # - Multiple "BUG=" lines, with varying amounts of whitespace. | |
171 # - For each BUG= line, bugs can be split by commas _or_ by whitespace (!) | |
153 entries = [] | 172 entries = [] |
154 for line in self.body.split('\n'): | 173 for line in self.body.split('\n'): |
155 match = re.match(r'^ *BUG *=(.*)', line) | 174 match = re.match(r'^ *BUG *=(.*)', line) |
156 if match: | 175 if match: |
157 for i in match.group(1).split(','): | 176 for i in match.group(1).split(','): |
158 entries.extend(filter(None, [x.strip() for x in i.split()])) | 177 entries.extend(filter(None, [x.strip() for x in i.split()])) |
159 | 178 |
179 # Try to parse the bugs. Handle lots of different formats: | |
180 # - The whole URL, from which we parse the project and bug. | |
181 # - A simple string that looks like "project:bug" | |
182 # - A string that looks like "bug", which will always refer to the previous | |
183 # tracker referenced (defaulting to the default tracker). | |
184 # | |
185 # We will create an "Issue" object for each bug. | |
160 issues = [] | 186 issues = [] |
161 last_tracker = DEFAULT_TRACKER | 187 last_tracker = DEFAULT_TRACKER |
162 regex = (r'http://code.google.com/p/(\S+)/issues/detail\?id=([0-9]+)' | 188 regex = (r'http://code.google.com/p/(\S+)/issues/detail\?id=([0-9]+)' |
163 r'|(\S+):([0-9]+)|(\b[0-9]+\b)') | 189 r'|(\S+):([0-9]+)|(\b[0-9]+\b)') |
164 | 190 |
165 for new_item in entries: | 191 for new_item in entries: |
166 bug_numbers = re.findall(regex, new_item) | 192 bug_numbers = re.findall(regex, new_item) |
167 for bug_tuple in bug_numbers: | 193 for bug_tuple in bug_numbers: |
168 if bug_tuple[0] and bug_tuple[1]: | 194 if bug_tuple[0] and bug_tuple[1]: |
169 issues.append(Issue(bug_tuple[0], bug_tuple[1], self._tracker_acc)) | 195 issues.append(Issue(bug_tuple[0], bug_tuple[1], self._tracker_acc)) |
170 last_tracker = bug_tuple[0] | 196 last_tracker = bug_tuple[0] |
171 elif bug_tuple[2] and bug_tuple[3]: | 197 elif bug_tuple[2] and bug_tuple[3]: |
172 issues.append(Issue(bug_tuple[2], bug_tuple[3], self._tracker_acc)) | 198 issues.append(Issue(bug_tuple[2], bug_tuple[3], self._tracker_acc)) |
173 last_tracker = bug_tuple[2] | 199 last_tracker = bug_tuple[2] |
174 elif bug_tuple[4]: | 200 elif bug_tuple[4]: |
175 issues.append(Issue(last_tracker, bug_tuple[4], self._tracker_acc)) | 201 issues.append(Issue(last_tracker, bug_tuple[4], self._tracker_acc)) |
176 | 202 |
203 # Sort the issues and return... | |
davidjames
2010/11/08 22:32:49
Only one period is needed here...
| |
177 issues.sort() | 204 issues.sort() |
178 return issues | 205 return issues |
179 | 206 |
180 def AsHTMLTableRow(self): | 207 def AsHTMLTableRow(self): |
181 """Returns HTML for this change, for printing as part of a table. | 208 """Returns HTML for this change, for printing as part of a table. |
182 | 209 |
183 Columns: Project, Date, Commit, Committer, Bugs, Subject. | 210 Columns: Project, Date, Commit, Committer, Bugs, Subject. |
184 | 211 |
185 Returns: | 212 Returns: |
186 A string usable as an HTML table row, like: | 213 A string usable as an HTML table row, like: |
(...skipping 11 matching lines...) Expand all Loading... | |
198 commit_desc = link_fmt % (url, self.commit[:8]) | 225 commit_desc = link_fmt % (url, self.commit[:8]) |
199 bug_str = '<br>'.join(bugs) | 226 bug_str = '<br>'.join(bugs) |
200 if not bug_str: | 227 if not bug_str: |
201 if (self.projectname == 'kernel-next' or | 228 if (self.projectname == 'kernel-next' or |
202 self.commit_email == 'chrome-bot@chromium.org'): | 229 self.commit_email == 'chrome-bot@chromium.org'): |
203 bug_str = 'not needed' | 230 bug_str = 'not needed' |
204 else: | 231 else: |
205 bug_str = '<font color="red">none</font>' | 232 bug_str = '<font color="red">none</font>' |
206 | 233 |
207 cols = [ | 234 cols = [ |
208 cgi.escape(self.projectname), | 235 cgi.escape(self.projectname), |
209 str(self.commit_date), | 236 str(self.commit_date), |
210 commit_desc, | 237 commit_desc, |
211 cgi.escape(self.commit_email), | 238 cgi.escape(self.commit_email), |
212 bug_str, | 239 bug_str, |
213 cgi.escape(self.subject[:100]), | 240 cgi.escape(self.subject[:100]), |
214 ] | 241 ] |
215 return '<tr><td>%s</td></tr>' % ('</td><td>'.join(cols)) | 242 return '<tr><td>%s</td></tr>' % ('</td><td>'.join(cols)) |
216 | 243 |
217 def __cmp__(self, other): | 244 def __cmp__(self, other): |
218 """Compare two Commit objects first by project name, then by date.""" | 245 """Compare two Commit objects first by project name, then by date.""" |
219 return (cmp(self.projectname, other.projectname) or | 246 return (cmp(self.projectname, other.projectname) or |
220 cmp(self.commit_date, other.commit_date)) | 247 cmp(self.commit_date, other.commit_date)) |
221 | 248 |
222 | 249 |
223 def _GrabChanges(path, tag1, tag2, tracker_acc): | 250 def _GrabChanges(path, tag1, tag2, tracker_acc): |
224 """Return list of commits to path between tag1 and tag2.""" | 251 """Return list of commits to path between tag1 and tag2. |
252 | |
253 Args: | |
254 path: One of the directories managed by repo. | |
255 tag1: The first of the two tags to pass to git log. | |
256 tag2: The second of the two tags to pass to git log. | |
257 tracker_acc: A tracker_access.TrackerAccess object. | |
258 | |
259 Returns: | |
260 A list of "Commit" objects. | |
261 """ | |
225 | 262 |
226 cmd = 'cd %s && git config --get remote.cros.projectname' % path | 263 cmd = 'cd %s && git config --get remote.cros.projectname' % path |
227 projectname = _GrabOutput(cmd).strip() | 264 projectname = _GrabOutput(cmd).strip() |
228 log_fmt = '%x00%H\t%ce\t%cd\t%s\t%b' | 265 log_fmt = '%x00%H\t%ce\t%cd\t%s\t%b' |
229 cmd_fmt = 'cd %s && git log --format="%s" --date=local "%s..%s"' | 266 cmd_fmt = 'cd %s && git log --format="%s" --date=local "%s..%s"' |
230 cmd = cmd_fmt % (path, log_fmt, tag1, tag2) | 267 cmd = cmd_fmt % (path, log_fmt, tag1, tag2) |
231 output = _GrabOutput(cmd) | 268 output = _GrabOutput(cmd) |
232 commits = [] | 269 commits = [] |
233 for log_data in output.split('\0')[1:]: | 270 for log_data in output.split('\0')[1:]: |
234 commit, commit_email, commit_date, subject, body = log_data.split('\t', 4) | 271 commit, commit_email, commit_date, subject, body = log_data.split('\t', 4) |
235 change = Commit(commit, projectname, commit_email, commit_date, subject, | 272 change = Commit(commit, projectname, commit_email, commit_date, subject, |
236 body, tracker_acc) | 273 body, tracker_acc) |
237 commits.append(change) | 274 commits.append(change) |
238 return commits | 275 return commits |
239 | 276 |
240 | 277 |
241 def _ParseArgs(): | 278 def _ParseArgs(): |
279 """Parse command-line arguments. | |
280 | |
281 Returns: | |
282 An optparse.OptionParser object. | |
283 """ | |
242 parser = optparse.OptionParser() | 284 parser = optparse.OptionParser() |
243 parser.add_option( | 285 parser.add_option( |
244 "--sort-by-date", dest="sort_by_date", default=False, | 286 '--sort-by-date', dest='sort_by_date', default=False, |
245 action='store_true', help="Sort commits by date.") | 287 action='store_true', help='Sort commits by date.') |
246 parser.add_option( | 288 parser.add_option( |
247 "--tracker-user", dest="tracker_user", default=None, | 289 '--tracker-user', dest='tracker_user', default=None, |
248 help="Specify a username to login to code.google.com.") | 290 help='Specify a username to login to code.google.com.') |
249 parser.add_option( | 291 parser.add_option( |
250 "--tracker-pass", dest="tracker_pass", default=None, | 292 '--tracker-pass', dest='tracker_pass', default=None, |
251 help="Specify a password to go w/ user.") | 293 help='Specify a password to go w/ user.') |
252 parser.add_option( | 294 parser.add_option( |
253 "--tracker-passfile", dest="tracker_passfile", default=None, | 295 '--tracker-passfile', dest='tracker_passfile', default=None, |
254 help="Specify a file containing a password to go w/ user.") | 296 help='Specify a file containing a password to go w/ user.') |
255 return parser.parse_args() | 297 return parser.parse_args() |
256 | 298 |
257 | 299 |
258 def main(): | 300 def main(): |
259 tags = _GrabTags() | 301 tags = _GrabTags() |
260 tag1 = None | 302 tag1 = None |
261 options, args = _ParseArgs() | 303 options, args = _ParseArgs() |
262 if len(args) == 2: | 304 if len(args) == 2: |
263 tag1, tag2 = args | 305 tag1, tag2 = args |
264 elif len(args) == 1: | 306 elif len(args) == 1: |
265 tag2, = args | 307 tag2, = args |
266 if tag2 in tags: | 308 if tag2 in tags: |
267 tag1 = tags[tags.index(tag2) + 1] | 309 tag2_index = tags.index(tag2) |
310 if tag2_index == len(tags) - 1: | |
311 print >>sys.stderr, 'No previous tag for %s' % tag2 | |
312 sys.exit(1) | |
313 tag1 = tags[tag2_index + 1] | |
268 else: | 314 else: |
269 print >>sys.stderr, 'Unrecognized tag: %s' % tag2 | 315 print >>sys.stderr, 'Unrecognized tag: %s' % tag2 |
270 sys.exit(1) | 316 sys.exit(1) |
271 else: | 317 else: |
272 print >>sys.stderr, 'Usage: %s [tag1] tag2' % sys.argv[0] | 318 print >>sys.stderr, 'Usage: %s [tag1] tag2' % sys.argv[0] |
273 print >>sys.stderr, 'If only one tag is specified, we view the differences' | 319 print >>sys.stderr, 'If only one tag is specified, we view the differences' |
274 print >>sys.stderr, 'between that tag and the previous tag. You can also' | 320 print >>sys.stderr, 'between that tag and the previous tag. You can also' |
275 print >>sys.stderr, 'specify cros/master to show differences with' | 321 print >>sys.stderr, 'specify cros/master to show differences with' |
276 print >>sys.stderr, 'tip-of-tree.' | 322 print >>sys.stderr, 'tip-of-tree.' |
277 print >>sys.stderr, 'E.g. %s %s cros/master' % (sys.argv[0], tags[0]) | 323 print >>sys.stderr, 'E.g. %s %s cros/master' % (sys.argv[0], tags[0]) |
278 sys.exit(1) | 324 sys.exit(1) |
279 | 325 |
280 if options.tracker_user is not None: | 326 if options.tracker_user is not None: |
281 # TODO(dianders): Once we install GData automatically, move the import | 327 # 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 | 328 # to the top of the file where it belongs. It's only here to allow |
283 # people to run the script without GData. | 329 # people to run the script without GData. |
284 try: | 330 try: |
285 import tracker_access | 331 import tracker_access |
286 except ImportError: | 332 except ImportError: |
287 print >>sys.stderr, INSTRS_FOR_GDATA | 333 print >>sys.stderr, INSTRS_FOR_GDATA |
288 sys.exit(1) | 334 sys.exit(1) |
289 if options.tracker_passfile is not None: | 335 if options.tracker_passfile is not None: |
290 options.tracker_pass = open(options.tracker_passfile, "r").read().strip() | 336 options.tracker_pass = open(options.tracker_passfile, 'r').read().strip() |
291 tracker_acc = tracker_access.TrackerAccess(options.tracker_user, | 337 tracker_acc = tracker_access.TrackerAccess(options.tracker_user, |
292 options.tracker_pass) | 338 options.tracker_pass) |
293 else: | 339 else: |
294 tracker_acc = None | 340 tracker_acc = None |
295 | 341 |
296 print >>sys.stderr, 'Finding differences between %s and %s' % (tag1, tag2) | 342 print >>sys.stderr, 'Finding differences between %s and %s' % (tag1, tag2) |
297 paths = _GrabDirs() | 343 paths = _GrabDirs() |
298 changes = [] | 344 changes = [] |
299 for path in paths: | 345 for path in paths: |
300 changes.extend(_GrabChanges(path, tag1, tag2, tracker_acc)) | 346 changes.extend(_GrabChanges(path, tag1, tag2, tracker_acc)) |
301 | 347 |
302 title = 'Changelog for %s to %s' % (tag1, tag2) | 348 title = 'Changelog for %s to %s' % (tag1, tag2) |
303 print '<html>' | 349 print '<html>' |
304 print '<head><title>%s</title></head>' % title | 350 print '<head><title>%s</title></head>' % title |
305 print '<h1>%s</h1>' % title | 351 print '<h1>%s</h1>' % title |
306 cols = ['Project', 'Date', 'Commit', 'Committer', 'Bugs', 'Subject'] | 352 cols = ['Project', 'Date', 'Commit', 'Committer', 'Bugs', 'Subject'] |
307 print '<table border="1" cellpadding="4">' | 353 print '<table border="1" cellpadding="4">' |
308 print '<tr><th>%s</th>' % ('</th><th>'.join(cols)) | 354 print '<tr><th>%s</th>' % ('</th><th>'.join(cols)) |
309 if options.sort_by_date: | 355 if options.sort_by_date: |
310 changes.sort(key=operator.attrgetter("commit_date")) | 356 changes.sort(key=operator.attrgetter('commit_date')) |
311 else: | 357 else: |
312 changes.sort() | 358 changes.sort() |
313 for change in changes: | 359 for change in changes: |
314 print change.AsHTMLTableRow() | 360 print change.AsHTMLTableRow() |
315 print '</table>' | 361 print '</table>' |
316 print '</html>' | 362 print '</html>' |
317 | 363 |
318 | 364 |
319 if __name__ == '__main__': | 365 if __name__ == '__main__': |
320 main() | 366 main() |
OLD | NEW |