Chromium Code Reviews| 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 |