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 24 matching lines...) Expand all Loading... | |
| 35 import sys | 35 import sys |
| 36 import urllib | 36 import urllib |
| 37 import urllib2 | 37 import urllib2 |
| 38 | 38 |
| 39 import auth | 39 import auth |
| 40 import fix_encoding | 40 import fix_encoding |
| 41 import gerrit_util | 41 import gerrit_util |
| 42 import rietveld | 42 import rietveld |
| 43 from third_party import upload | 43 from third_party import upload |
| 44 | 44 |
| 45 import auth | |
| 46 from third_party import httplib2 | |
| 47 | |
| 45 try: | 48 try: |
| 46 from dateutil.relativedelta import relativedelta # pylint: disable=F0401 | 49 from dateutil.relativedelta import relativedelta # pylint: disable=F0401 |
| 47 except ImportError: | 50 except ImportError: |
| 48 print 'python-dateutil package required' | 51 print 'python-dateutil package required' |
| 49 exit(1) | 52 exit(1) |
| 50 | 53 |
| 51 # python-keyring provides easy access to the system keyring. | 54 # python-keyring provides easy access to the system keyring. |
| 52 try: | 55 try: |
| 53 import keyring # pylint: disable=W0611,F0401 | 56 import keyring # pylint: disable=W0611,F0401 |
| 54 except ImportError: | 57 except ImportError: |
| (...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 93 gerrit_instances = [ | 96 gerrit_instances = [ |
| 94 { | 97 { |
| 95 'url': 'chromium-review.googlesource.com', | 98 'url': 'chromium-review.googlesource.com', |
| 96 'shorturl': 'crosreview.com', | 99 'shorturl': 'crosreview.com', |
| 97 }, | 100 }, |
| 98 { | 101 { |
| 99 'url': 'chrome-internal-review.googlesource.com', | 102 'url': 'chrome-internal-review.googlesource.com', |
| 100 'shorturl': 'crosreview.com/i', | 103 'shorturl': 'crosreview.com/i', |
| 101 }, | 104 }, |
| 102 { | 105 { |
| 103 'host': 'gerrit.chromium.org', | 106 'host': 'gerrit.chromium.org', |
|
Vadim Sh.
2015/06/11 01:37:31
this host has been turn off today, can be removed
seanmccullough
2015/06/11 02:05:22
Done.
| |
| 104 'port': 29418, | 107 'port': 29418, |
| 105 }, | 108 }, |
| 106 ] | 109 ] |
| 107 | 110 |
| 108 google_code_projects = [ | 111 google_code_projects = [ |
| 109 { | 112 { |
| 110 'name': 'brillo', | 113 'name': 'brillo', |
| 111 'shorturl': 'brbug.com', | 114 'shorturl': 'brbug.com', |
| 112 }, | 115 }, |
| 113 { | 116 { |
| (...skipping 12 matching lines...) Expand all Loading... | |
| 126 }, | 129 }, |
| 127 { | 130 { |
| 128 'name': 'gyp', | 131 'name': 'gyp', |
| 129 }, | 132 }, |
| 130 { | 133 { |
| 131 'name': 'skia', | 134 'name': 'skia', |
| 132 }, | 135 }, |
| 133 ] | 136 ] |
| 134 | 137 |
| 135 # Uses ClientLogin to authenticate the user for Google Code issue trackers. | 138 # Uses ClientLogin to authenticate the user for Google Code issue trackers. |
| 136 def get_auth_token(email): | 139 def get_auth_token(email): |
|
Vadim Sh.
2015/06/11 01:37:31
this should be eventually deleted, it's broken
seanmccullough
2015/06/11 02:05:22
Done.
| |
| 137 # KeyringCreds will use the system keyring on the first try, and prompt for | 140 # KeyringCreds will use the system keyring on the first try, and prompt for |
| 138 # a password on the next ones. | 141 # a password on the next ones. |
| 139 creds = upload.KeyringCreds('code.google.com', 'code.google.com', email) | 142 creds = upload.KeyringCreds('code.google.com', 'code.google.com', email) |
| 140 for _ in xrange(3): | 143 for _ in xrange(3): |
| 141 email, password = creds.GetUserCredentials() | 144 email, password = creds.GetUserCredentials() |
| 142 url = 'https://www.google.com/accounts/ClientLogin' | 145 url = 'https://www.google.com/accounts/ClientLogin' |
| 143 data = urllib.urlencode({ | 146 data = urllib.urlencode({ |
| 144 'Email': email, | 147 'Email': email, |
| 145 'Passwd': password, | 148 'Passwd': password, |
| 146 'service': 'code', | 149 'service': 'code', |
| (...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 223 self.user = options.user | 226 self.user = options.user |
| 224 self.changes = [] | 227 self.changes = [] |
| 225 self.reviews = [] | 228 self.reviews = [] |
| 226 self.issues = [] | 229 self.issues = [] |
| 227 self.check_cookies() | 230 self.check_cookies() |
| 228 self.google_code_auth_token = None | 231 self.google_code_auth_token = None |
| 229 | 232 |
| 230 # Check the codereview cookie jar to determine which Rietveld instances to | 233 # Check the codereview cookie jar to determine which Rietveld instances to |
| 231 # authenticate to. | 234 # authenticate to. |
| 232 def check_cookies(self): | 235 def check_cookies(self): |
| 233 cookie_file = os.path.expanduser('~/.codereview_upload_cookies') | 236 cookie_file = os.path.expanduser('~/.codereview_upload_cookies') |
|
Vadim Sh.
2015/06/11 01:37:31
it's not directly related to OAuth code.google.com
seanmccullough
2015/06/11 02:05:22
removed it, now it just checks "has_cookie"
| |
| 234 if not os.path.exists(cookie_file): | 237 if not os.path.exists(cookie_file): |
| 235 print 'No Rietveld cookie file found.' | 238 print 'No Rietveld cookie file found.' |
| 236 cookie_jar = [] | 239 cookie_jar = [] |
| 237 else: | 240 else: |
| 238 cookie_jar = cookielib.MozillaCookieJar(cookie_file) | 241 cookie_jar = cookielib.MozillaCookieJar(cookie_file) |
| 239 try: | 242 try: |
| 240 cookie_jar.load() | 243 cookie_jar.load() |
| 241 print 'Found cookie file: %s' % cookie_file | 244 print 'Found cookie file: %s' % cookie_file |
| 242 except (cookielib.LoadError, IOError): | 245 except (cookielib.LoadError, IOError): |
| 243 print 'Error loading Rietveld cookie file: %s' % cookie_file | 246 print 'Error loading Rietveld cookie file: %s' % cookie_file |
| 244 cookie_jar = [] | 247 cookie_jar = [] |
| 245 | 248 |
| 246 filtered_instances = [] | 249 filtered_instances = [] |
| 247 | 250 |
| 248 def has_cookie(instance): | 251 def has_cookie(instance): |
|
Vadim Sh.
2015/06/11 01:37:31
this can be reformulated as:
a = auth.get_authent
seanmccullough
2015/06/11 02:05:22
Done.
| |
| 249 for cookie in cookie_jar: | 252 for cookie in cookie_jar: |
| 250 if cookie.name == 'SACSID' and cookie.domain == instance['url']: | 253 if cookie.name == 'SACSID' and cookie.domain == instance['url']: |
| 251 return True | 254 return True |
| 252 if self.options.auth: | 255 if self.options.auth: |
| 253 return get_yes_or_no('No cookie found for %s. Authorize for this ' | 256 return get_yes_or_no('No cookie found for %s. Authorize for this ' |
| 254 'instance? (may require application-specific ' | 257 'instance? (may require application-specific ' |
| 255 'password)' % instance['url']) | 258 'password)' % instance['url']) |
| 256 filtered_instances.append(instance) | 259 filtered_instances.append(instance) |
| 257 return False | 260 return False |
| 258 | 261 |
| (...skipping 195 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 454 replies = filter(lambda r: 'author' in r and 'email' in r['author'], | 457 replies = filter(lambda r: 'author' in r and 'email' in r['author'], |
| 455 replies) | 458 replies) |
| 456 for reply in replies: | 459 for reply in replies: |
| 457 ret.append({ | 460 ret.append({ |
| 458 'author': reply['author']['email'], | 461 'author': reply['author']['email'], |
| 459 'created': datetime_from_gerrit(reply['date']), | 462 'created': datetime_from_gerrit(reply['date']), |
| 460 'content': reply['message'], | 463 'content': reply['message'], |
| 461 }) | 464 }) |
| 462 return ret | 465 return ret |
| 463 | 466 |
| 464 def google_code_issue_search(self, instance): | 467 def project_hosting_issue_search(self, instance): |
| 465 time_format = '%Y-%m-%dT%T' | 468 auth_config = auth.extract_auth_config_from_options(self.options) |
| 466 # See http://code.google.com/p/support/wiki/IssueTrackerAPI | 469 authenticator = auth.get_authenticator_for_host( |
| 467 # q=<owner>@chromium.org does a full text search for <owner>@chromium.org. | 470 "https://code.google.com", auth_config) |
|
Vadim Sh.
2015/06/11 01:37:32
just "code.google.com" here should work too
seanmccullough
2015/06/11 02:05:22
Done.
| |
| 468 # This will accept the issue if owner is the owner or in the cc list. Might | 471 http = authenticator.authorize(httplib2.Http()) |
| 469 # have some false positives, though. | 472 url = "https://www.googleapis.com/projecthosting/v2/projects/%s/issues" % ( |
| 473 instance["name"]) | |
| 474 epoch = datetime.utcfromtimestamp(0) | |
| 475 user_str = '%s@chromium.org' % self.user | |
| 470 | 476 |
| 471 # Don't filter normally on modified_before because it can filter out things | 477 query_data = urllib.urlencode({ |
| 472 # that were modified in the time period and then modified again after it. | 478 'maxResults': 10000, |
| 473 gcode_url = ('https://code.google.com/feeds/issues/p/%s/issues/full' % | 479 'q': '%s' % user_str, |
|
Vadim Sh.
2015/06/11 01:37:31
'q': user_str
seanmccullough
2015/06/11 02:05:22
Done.
| |
| 474 instance['name']) | 480 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(), |
| 475 | 481 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(), |
| 476 gcode_data = urllib.urlencode({ | |
| 477 'alt': 'json', | |
| 478 'max-results': '100000', | |
| 479 'q': '%s' % self.user, | |
| 480 'published-max': self.modified_before.strftime(time_format), | |
| 481 'updated-min': self.modified_after.strftime(time_format), | |
| 482 }) | 482 }) |
| 483 | 483 url = url + '?' + query_data |
| 484 opener = urllib2.build_opener() | 484 _, body = http.request(url) |
|
Vadim Sh.
2015/06/11 01:37:31
this thing will raise auth.AuthenticationError if
seanmccullough
2015/06/11 02:05:22
Done.
| |
| 485 if self.google_code_auth_token: | 485 content = json.loads(body) |
| 486 opener.addheaders = [('Authorization', 'GoogleLogin auth=%s' % | 486 if not content: |
| 487 self.google_code_auth_token)] | 487 print "Unable to parse %s response from projecthosting." % ( |
| 488 gcode_json = None | 488 instance["name"]) |
| 489 try: | |
| 490 gcode_get = opener.open(gcode_url + '?' + gcode_data) | |
| 491 gcode_json = json.load(gcode_get) | |
| 492 gcode_get.close() | |
| 493 except urllib2.HTTPError, _: | |
| 494 print 'Unable to access ' + instance['name'] + ' issue tracker.' | |
| 495 | |
| 496 if not gcode_json or 'entry' not in gcode_json['feed']: | |
| 497 return [] | 489 return [] |
| 498 | 490 |
| 499 issues = gcode_json['feed']['entry'] | 491 issues = [] |
| 500 issues = map(partial(self.process_google_code_issue, instance), issues) | 492 if 'items' in content: |
| 501 issues = filter(self.filter_issue, issues) | 493 items = content['items'] |
| 502 issues = sorted(issues, key=lambda i: i['modified'], reverse=True) | 494 for item in items: |
| 495 issue = { | |
| 496 "header": item["title"], | |
| 497 "created": item["published"], | |
| 498 "modified": item["updated"], | |
| 499 "author": item["author"]["name"], | |
| 500 "url": "https://code.google.com/p/%s/issues/detail?id=%s" % ( | |
| 501 instance["name"], item["id"]), | |
| 502 "comments": [] | |
| 503 } | |
| 504 if 'owner' in item: | |
| 505 issue['owner'] = item['owner']['name'] | |
| 506 else: | |
| 507 issue['owner'] = 'None' | |
| 508 if issue['owner'] == user_str or issue['author'] == user_str: | |
| 509 issues.append(issue) | |
| 510 | |
| 503 return issues | 511 return issues |
| 504 | 512 |
| 505 def process_google_code_issue(self, project, issue): | |
| 506 ret = {} | |
| 507 ret['created'] = datetime_from_google_code(issue['published']['$t']) | |
| 508 ret['modified'] = datetime_from_google_code(issue['updated']['$t']) | |
| 509 | |
| 510 ret['owner'] = '' | |
| 511 if 'issues$owner' in issue: | |
| 512 ret['owner'] = issue['issues$owner']['issues$username']['$t'] | |
| 513 ret['author'] = issue['author'][0]['name']['$t'] | |
| 514 | |
| 515 if 'shorturl' in project: | |
| 516 issue_id = issue['id']['$t'] | |
| 517 issue_id = issue_id[issue_id.rfind('/') + 1:] | |
| 518 ret['url'] = 'http://%s/%d' % (project['shorturl'], int(issue_id)) | |
| 519 else: | |
| 520 issue_url = issue['link'][1] | |
| 521 if issue_url['rel'] != 'alternate': | |
| 522 raise RuntimeError | |
| 523 ret['url'] = issue_url['href'] | |
| 524 ret['header'] = issue['title']['$t'] | |
| 525 | |
| 526 ret['replies'] = self.get_google_code_issue_replies(issue) | |
| 527 return ret | |
| 528 | |
| 529 def get_google_code_issue_replies(self, issue): | |
| 530 """Get all the comments on the issue.""" | |
| 531 replies_url = issue['link'][0] | |
| 532 if replies_url['rel'] != 'replies': | |
| 533 raise RuntimeError | |
| 534 | |
| 535 replies_data = urllib.urlencode({ | |
| 536 'alt': 'json', | |
| 537 'fields': 'entry(published,author,content)', | |
| 538 }) | |
| 539 | |
| 540 opener = urllib2.build_opener() | |
| 541 opener.addheaders = [('Authorization', 'GoogleLogin auth=%s' % | |
| 542 self.google_code_auth_token)] | |
| 543 try: | |
| 544 replies_get = opener.open(replies_url['href'] + '?' + replies_data) | |
| 545 except urllib2.HTTPError, _: | |
| 546 return [] | |
| 547 | |
| 548 replies_json = json.load(replies_get) | |
| 549 replies_get.close() | |
| 550 return self.process_google_code_issue_replies(replies_json) | |
| 551 | |
| 552 @staticmethod | |
| 553 def process_google_code_issue_replies(replies): | |
| 554 if 'entry' not in replies['feed']: | |
| 555 return [] | |
| 556 | |
| 557 ret = [] | |
| 558 for entry in replies['feed']['entry']: | |
| 559 e = {} | |
| 560 e['created'] = datetime_from_google_code(entry['published']['$t']) | |
| 561 e['content'] = entry['content']['$t'] | |
| 562 e['author'] = entry['author'][0]['name']['$t'] | |
| 563 ret.append(e) | |
| 564 return ret | |
| 565 | |
| 566 def print_heading(self, heading): | 513 def print_heading(self, heading): |
| 567 print | 514 print |
| 568 print self.options.output_format_heading.format(heading=heading) | 515 print self.options.output_format_heading.format(heading=heading) |
| 569 | 516 |
| 570 def print_change(self, change): | 517 def print_change(self, change): |
| 571 optional_values = { | 518 optional_values = { |
| 572 'reviewers': ', '.join(change['reviewers']) | 519 'reviewers': ', '.join(change['reviewers']) |
| 573 } | 520 } |
| 574 self.print_generic(self.options.output_format, | 521 self.print_generic(self.options.output_format, |
| 575 self.options.output_format_changes, | 522 self.options.output_format_changes, |
| (...skipping 99 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 675 self.reviews += reviews | 622 self.reviews += reviews |
| 676 | 623 |
| 677 def print_reviews(self): | 624 def print_reviews(self): |
| 678 if self.reviews: | 625 if self.reviews: |
| 679 self.print_heading('Reviews') | 626 self.print_heading('Reviews') |
| 680 for review in self.reviews: | 627 for review in self.reviews: |
| 681 self.print_review(review) | 628 self.print_review(review) |
| 682 | 629 |
| 683 def get_issues(self): | 630 def get_issues(self): |
| 684 for project in google_code_projects: | 631 for project in google_code_projects: |
| 685 self.issues += self.google_code_issue_search(project) | 632 self.issues += self.project_hosting_issue_search(project) |
| 686 | 633 |
| 687 def print_issues(self): | 634 def print_issues(self): |
| 688 if self.issues: | 635 if self.issues: |
| 689 self.print_heading('Issues') | 636 self.print_heading('Issues') |
| 690 for issue in self.issues: | 637 for issue in self.issues: |
| 691 self.print_issue(issue) | 638 self.print_issue(issue) |
| 692 | 639 |
| 693 def print_activity(self): | 640 def print_activity(self): |
| 694 self.print_changes() | 641 self.print_changes() |
| 695 self.print_reviews() | 642 self.print_reviews() |
| (...skipping 167 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 863 | 810 |
| 864 if __name__ == '__main__': | 811 if __name__ == '__main__': |
| 865 # Fix encoding to support non-ascii issue titles. | 812 # Fix encoding to support non-ascii issue titles. |
| 866 fix_encoding.fix_encoding() | 813 fix_encoding.fix_encoding() |
| 867 | 814 |
| 868 try: | 815 try: |
| 869 sys.exit(main()) | 816 sys.exit(main()) |
| 870 except KeyboardInterrupt: | 817 except KeyboardInterrupt: |
| 871 sys.stderr.write('interrupted\n') | 818 sys.stderr.write('interrupted\n') |
| 872 sys.exit(1) | 819 sys.exit(1) |
| OLD | NEW |