Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Get stats about your activity. | 6 """Get stats about your activity. |
| 7 | 7 |
| 8 Example: | 8 Example: |
| 9 - my_activity.py for stats for the current week (last week on mondays). | 9 - my_activity.py for stats for the current week (last week on mondays). |
| 10 - my_activity.py -Q for stats for last quarter. | 10 - my_activity.py -Q for stats for last quarter. |
| (...skipping 98 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 109 { | 109 { |
| 110 'url': 'breakpad.appspot.com', | 110 'url': 'breakpad.appspot.com', |
| 111 'supports_owner_modified_query': False, | 111 'supports_owner_modified_query': False, |
| 112 'requires_auth': False, | 112 'requires_auth': False, |
| 113 'email_domain': 'chromium.org', | 113 'email_domain': 'chromium.org', |
| 114 }, | 114 }, |
| 115 ] | 115 ] |
| 116 | 116 |
| 117 gerrit_instances = [ | 117 gerrit_instances = [ |
| 118 { | 118 { |
| 119 'url': 'gerrit.chromium.org', | 119 'url': 'chromium-review.googlesource.com', |
| 120 'port': 29418, | |
| 121 'shorturl': 'crosreview.com', | 120 'shorturl': 'crosreview.com', |
| 122 }, | 121 }, |
| 122 # TODO: chrome-internal-review requires login credentials. Enable once login | |
| 123 # support is added to this client. See crbug.com/281695. | |
| 124 #{ | |
| 125 # 'url': 'chrome-internal-review.googlesource.com', | |
| 126 # 'shorturl': 'crosreview.com/i', | |
| 127 #}, | |
| 123 { | 128 { |
| 124 'url': 'gerrit-int.chromium.org', | 129 'host': 'gerrit.chromium.org', |
| 130 'port': 29418, | |
| 131 }, | |
| 132 { | |
| 133 'host': 'gerrit-int.chromium.org', | |
| 125 'port': 29419, | 134 'port': 29419, |
| 126 'shorturl': 'crosreview.com/i', | |
| 127 }, | 135 }, |
| 128 ] | 136 ] |
| 129 | 137 |
| 130 google_code_projects = [ | 138 google_code_projects = [ |
| 131 { | 139 { |
| 132 'name': 'chromium', | 140 'name': 'chromium', |
| 133 'shorturl': 'crbug.com', | 141 'shorturl': 'crbug.com', |
| 134 }, | 142 }, |
| 135 { | 143 { |
| 136 'name': 'chromium-os', | 144 'name': 'chromium-os', |
| (...skipping 95 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 232 | 240 |
| 233 def get_yes_or_no(msg): | 241 def get_yes_or_no(msg): |
| 234 while True: | 242 while True: |
| 235 response = raw_input(msg + ' yes/no [no] ') | 243 response = raw_input(msg + ' yes/no [no] ') |
| 236 if response == 'y' or response == 'yes': | 244 if response == 'y' or response == 'yes': |
| 237 return True | 245 return True |
| 238 elif not response or response == 'n' or response == 'no': | 246 elif not response or response == 'n' or response == 'no': |
| 239 return False | 247 return False |
| 240 | 248 |
| 241 | 249 |
| 250 def datetime_from_gerrit(date_string): | |
| 251 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000') | |
| 252 | |
| 253 | |
| 242 def datetime_from_rietveld(date_string): | 254 def datetime_from_rietveld(date_string): |
| 243 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f') | 255 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f') |
| 244 | 256 |
| 245 | 257 |
| 246 def datetime_from_google_code(date_string): | 258 def datetime_from_google_code(date_string): |
| 247 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S.%fZ') | 259 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S.%fZ') |
| 248 | 260 |
| 249 | 261 |
| 250 class MyActivity(object): | 262 class MyActivity(object): |
| 251 def __init__(self, options): | 263 def __init__(self, options): |
| (...skipping 117 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 369 def process_rietveld_replies(replies): | 381 def process_rietveld_replies(replies): |
| 370 ret = [] | 382 ret = [] |
| 371 for reply in replies: | 383 for reply in replies: |
| 372 r = {} | 384 r = {} |
| 373 r['author'] = reply['sender'] | 385 r['author'] = reply['sender'] |
| 374 r['created'] = datetime_from_rietveld(reply['date']) | 386 r['created'] = datetime_from_rietveld(reply['date']) |
| 375 r['content'] = '' | 387 r['content'] = '' |
| 376 ret.append(r) | 388 ret.append(r) |
| 377 return ret | 389 return ret |
| 378 | 390 |
| 391 @staticmethod | |
| 392 def gerrit_changes_over_ssh(instance, filters): | |
| 393 # See https://review.openstack.org/Documentation/cmd-query.html | |
| 394 # Gerrit doesn't allow filtering by created time, only modified time. | |
| 395 gquery_cmd = ['ssh', '-p', str(instance['port']), instance['host'], | |
| 396 'gerrit', 'query', | |
| 397 '--format', 'JSON', | |
| 398 '--comments', | |
| 399 '--'] + filters | |
| 400 [stdout, _] = subprocess.Popen(gquery_cmd, stdout=subprocess.PIPE, | |
| 401 stderr=subprocess.PIPE).communicate() | |
| 402 issues = str(stdout).split('\n')[:-2] | |
| 403 return map(json.loads, issues) | |
| 404 | |
| 405 @staticmethod | |
| 406 def gerrit_changes_over_rest(instance, filters): | |
| 407 # See https://gerrit-review.googlesource.com/Documentation/rest-api.html | |
| 408 # Gerrit doesn't allow filtering by created time, only modified time. | |
| 409 args = urllib.urlencode([ | |
| 410 ('q', ' '.join(filters)), | |
| 411 ('o', 'MESSAGES'), | |
| 412 ('o', 'LABELS')]) | |
| 413 rest_url = 'https://%s/changes/?%s' % (instance['url'], args) | |
| 414 | |
| 415 req = urllib2.Request(rest_url, headers={'Accept': 'text/plain'}) | |
| 416 try: | |
| 417 response = urllib2.urlopen(req) | |
| 418 stdout = response.read() | |
| 419 except urllib2.HTTPError, e: | |
| 420 print 'Looking up %r: %s' % (rest_url, e) | |
| 421 return [] | |
| 422 | |
| 423 # Check that the returned JSON starts with the right marker. | |
| 424 if stdout[0:5] != ")]}'\n": | |
|
cjhopman
2013/09/10 19:57:51
maybe print a short error here too?
| |
| 425 return [] | |
| 426 return json.loads(stdout[5:]) | |
| 427 | |
| 379 def gerrit_search(self, instance, owner=None, reviewer=None): | 428 def gerrit_search(self, instance, owner=None, reviewer=None): |
| 380 max_age = datetime.today() - self.modified_after | 429 max_age = datetime.today() - self.modified_after |
| 381 max_age = max_age.days * 24 * 3600 + max_age.seconds | 430 max_age = max_age.days * 24 * 3600 + max_age.seconds |
| 431 user_filter = 'owner:%s' % owner if owner else 'reviewer:%s' % reviewer | |
| 432 filters = ['-age:%ss' % max_age, user_filter] | |
| 382 | 433 |
| 383 # See https://review.openstack.org/Documentation/cmd-query.html | 434 # Determine the gerrit interface to use: SSH or REST API: |
| 384 # Gerrit doesn't allow filtering by created time, only modified time. | 435 if 'host' in instance: |
| 385 user_filter = 'owner:%s' % owner if owner else 'reviewer:%s' % reviewer | 436 issues = self.gerrit_changes_over_ssh(instance, filters) |
| 386 gquery_cmd = ['ssh', '-p', str(instance['port']), instance['url'], | 437 issues = [self.process_gerrit_ssh_issue(instance, issue) |
| 387 'gerrit', 'query', | 438 for issue in issues] |
| 388 '--format', 'JSON', | 439 elif 'url' in instance: |
| 389 '--comments', | 440 issues = self.gerrit_changes_over_rest(instance, filters) |
| 390 '--', | 441 issues = [self.process_gerrit_rest_issue(instance, issue) |
| 391 '-age:%ss' % str(max_age), | 442 for issue in issues] |
| 392 user_filter] | 443 else: |
| 393 [stdout, _] = subprocess.Popen(gquery_cmd, stdout=subprocess.PIPE, | 444 raise Exception("Invalid gerrit_instances configuration.") |
| 394 stderr=subprocess.PIPE).communicate() | |
| 395 issues = str(stdout).split('\n')[:-2] | |
| 396 issues = map(json.loads, issues) | |
| 397 | 445 |
| 398 # TODO(cjhopman): should we filter abandoned changes? | 446 # TODO(cjhopman): should we filter abandoned changes? |
| 399 issues = [self.process_gerrit_issue(instance, issue) for issue in issues] | |
| 400 issues = filter(self.filter_issue, issues) | 447 issues = filter(self.filter_issue, issues) |
| 401 issues = sorted(issues, key=lambda i: i['modified'], reverse=True) | 448 issues = sorted(issues, key=lambda i: i['modified'], reverse=True) |
| 402 | 449 |
| 403 return issues | 450 return issues |
| 404 | 451 |
| 405 def process_gerrit_issue(self, instance, issue): | 452 def process_gerrit_ssh_issue(self, instance, issue): |
| 406 ret = {} | 453 ret = {} |
| 407 ret['review_url'] = issue['url'] | 454 ret['review_url'] = issue['url'] |
| 408 if 'shorturl' in instance: | 455 if 'shorturl' in instance: |
| 409 ret['review_url'] = 'http://%s/%s' % (instance['shorturl'], | 456 ret['review_url'] = 'http://%s/%s' % (instance['shorturl'], |
| 410 issue['number']) | 457 issue['number']) |
| 411 ret['header'] = issue['subject'] | 458 ret['header'] = issue['subject'] |
| 412 ret['owner'] = issue['owner']['email'] | 459 ret['owner'] = issue['owner']['email'] |
| 413 ret['author'] = ret['owner'] | 460 ret['author'] = ret['owner'] |
| 414 ret['created'] = datetime.fromtimestamp(issue['createdOn']) | 461 ret['created'] = datetime.fromtimestamp(issue['createdOn']) |
| 415 ret['modified'] = datetime.fromtimestamp(issue['lastUpdated']) | 462 ret['modified'] = datetime.fromtimestamp(issue['lastUpdated']) |
| 416 if 'comments' in issue: | 463 if 'comments' in issue: |
| 417 ret['replies'] = self.process_gerrit_issue_replies(issue['comments']) | 464 ret['replies'] = self.process_gerrit_ssh_issue_replies(issue['comments']) |
| 418 else: | 465 else: |
| 419 ret['replies'] = [] | 466 ret['replies'] = [] |
| 420 ret['reviewers'] = set() | 467 ret['reviewers'] = set() |
| 421 for reply in ret['replies']: | 468 for reply in ret['replies']: |
| 422 if reply['author'] != ret['author']: | 469 if reply['author'] != ret['author']: |
| 423 ret['reviewers'].add(reply['author']) | 470 ret['reviewers'].add(reply['author']) |
| 424 return ret | 471 return ret |
| 425 | 472 |
| 426 @staticmethod | 473 @staticmethod |
| 427 def process_gerrit_issue_replies(replies): | 474 def process_gerrit_ssh_issue_replies(replies): |
| 428 ret = [] | 475 ret = [] |
| 429 replies = filter(lambda r: 'email' in r['reviewer'], replies) | 476 replies = filter(lambda r: 'email' in r['reviewer'], replies) |
| 430 for reply in replies: | 477 for reply in replies: |
| 431 r = {} | 478 r = {} |
| 432 r['author'] = reply['reviewer']['email'] | 479 r['author'] = reply['reviewer']['email'] |
| 433 r['created'] = datetime.fromtimestamp(reply['timestamp']) | 480 r['created'] = datetime.fromtimestamp(reply['timestamp']) |
| 434 r['content'] = '' | 481 r['content'] = '' |
| 435 ret.append(r) | 482 ret.append(r) |
| 436 return ret | 483 return ret |
| 437 | 484 |
| 485 def process_gerrit_rest_issue(self, instance, issue): | |
| 486 ret = {} | |
| 487 ret['review_url'] = 'http://%s/%s' % (instance['url'], issue['_number']) | |
| 488 if 'shorturl' in instance: | |
| 489 ret['review_url'] = 'http://%s/%s' % (instance['shorturl'], | |
| 490 issue['_number']) | |
| 491 ret['header'] = issue['subject'] | |
| 492 ret['owner'] = issue['owner']['email'] | |
| 493 ret['author'] = ret['owner'] | |
| 494 ret['created'] = datetime_from_gerrit(issue['created']) | |
| 495 ret['modified'] = datetime_from_gerrit(issue['updated']) | |
| 496 if 'messages' in issue: | |
| 497 ret['replies'] = self.process_gerrit_rest_issue_replies(issue['messages']) | |
| 498 else: | |
| 499 ret['replies'] = [] | |
| 500 ret['reviewers'] = set() | |
| 501 for reply in ret['replies']: | |
| 502 if reply['author'] != ret['author']: | |
| 503 ret['reviewers'].add(reply['author']) | |
| 504 return ret | |
| 505 | |
| 506 @staticmethod | |
| 507 def process_gerrit_rest_issue_replies(replies): | |
| 508 ret = [] | |
| 509 replies = filter(lambda r: 'email' in r['author'], replies) | |
| 510 for reply in replies: | |
| 511 r = {} | |
| 512 r['author'] = reply['author']['email'] | |
| 513 r['created'] = datetime_from_gerrit(reply['date']) | |
| 514 r['content'] = reply['message'] | |
| 515 ret.append(r) | |
| 516 return ret | |
| 517 | |
| 438 def google_code_issue_search(self, instance): | 518 def google_code_issue_search(self, instance): |
| 439 time_format = '%Y-%m-%dT%T' | 519 time_format = '%Y-%m-%dT%T' |
| 440 # See http://code.google.com/p/support/wiki/IssueTrackerAPI | 520 # See http://code.google.com/p/support/wiki/IssueTrackerAPI |
| 441 # q=<owner>@chromium.org does a full text search for <owner>@chromium.org. | 521 # q=<owner>@chromium.org does a full text search for <owner>@chromium.org. |
| 442 # This will accept the issue if owner is the owner or in the cc list. Might | 522 # This will accept the issue if owner is the owner or in the cc list. Might |
| 443 # have some false positives, though. | 523 # have some false positives, though. |
| 444 | 524 |
| 445 # Don't filter normally on modified_before because it can filter out things | 525 # Don't filter normally on modified_before because it can filter out things |
| 446 # that were modified in the time period and then modified again after it. | 526 # that were modified in the time period and then modified again after it. |
| 447 gcode_url = ('https://code.google.com/feeds/issues/p/%s/issues/full' % | 527 gcode_url = ('https://code.google.com/feeds/issues/p/%s/issues/full' % |
| (...skipping 577 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1025 print '\n\n\n' | 1105 print '\n\n\n' |
| 1026 | 1106 |
| 1027 my_activity.print_changes() | 1107 my_activity.print_changes() |
| 1028 my_activity.print_reviews() | 1108 my_activity.print_reviews() |
| 1029 my_activity.print_issues() | 1109 my_activity.print_issues() |
| 1030 return 0 | 1110 return 0 |
| 1031 | 1111 |
| 1032 | 1112 |
| 1033 if __name__ == '__main__': | 1113 if __name__ == '__main__': |
| 1034 sys.exit(main()) | 1114 sys.exit(main()) |
| OLD | NEW |