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(deymo): chrome-internal-review requires login credentials. Enable once |
| 123 # login 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 # Drop the last line of the output with the stats. |
| 403 issues = stdout.splitlines()[:-1] |
| 404 return map(json.loads, issues) |
| 405 |
| 406 @staticmethod |
| 407 def gerrit_changes_over_rest(instance, filters): |
| 408 # See https://gerrit-review.googlesource.com/Documentation/rest-api.html |
| 409 # Gerrit doesn't allow filtering by created time, only modified time. |
| 410 args = urllib.urlencode([ |
| 411 ('q', ' '.join(filters)), |
| 412 ('o', 'MESSAGES'), |
| 413 ('o', 'LABELS')]) |
| 414 rest_url = 'https://%s/changes/?%s' % (instance['url'], args) |
| 415 |
| 416 req = urllib2.Request(rest_url, headers={'Accept': 'text/plain'}) |
| 417 try: |
| 418 response = urllib2.urlopen(req) |
| 419 stdout = response.read() |
| 420 except urllib2.HTTPError, e: |
| 421 print 'ERROR: Looking up %r: %s' % (rest_url, e) |
| 422 return [] |
| 423 |
| 424 # Check that the returned JSON starts with the right marker. |
| 425 if stdout[:5] != ")]}'\n": |
| 426 print 'ERROR: Marker not found on REST API response: %r' % stdout[:5] |
| 427 return [] |
| 428 return json.loads(stdout[5:]) |
| 429 |
379 def gerrit_search(self, instance, owner=None, reviewer=None): | 430 def gerrit_search(self, instance, owner=None, reviewer=None): |
380 max_age = datetime.today() - self.modified_after | 431 max_age = datetime.today() - self.modified_after |
381 max_age = max_age.days * 24 * 3600 + max_age.seconds | 432 max_age = max_age.days * 24 * 3600 + max_age.seconds |
| 433 user_filter = 'owner:%s' % owner if owner else 'reviewer:%s' % reviewer |
| 434 filters = ['-age:%ss' % max_age, user_filter] |
382 | 435 |
383 # See https://review.openstack.org/Documentation/cmd-query.html | 436 # Determine the gerrit interface to use: SSH or REST API: |
384 # Gerrit doesn't allow filtering by created time, only modified time. | 437 if 'host' in instance: |
385 user_filter = 'owner:%s' % owner if owner else 'reviewer:%s' % reviewer | 438 issues = self.gerrit_changes_over_ssh(instance, filters) |
386 gquery_cmd = ['ssh', '-p', str(instance['port']), instance['url'], | 439 issues = [self.process_gerrit_ssh_issue(instance, issue) |
387 'gerrit', 'query', | 440 for issue in issues] |
388 '--format', 'JSON', | 441 elif 'url' in instance: |
389 '--comments', | 442 issues = self.gerrit_changes_over_rest(instance, filters) |
390 '--', | 443 issues = [self.process_gerrit_rest_issue(instance, issue) |
391 '-age:%ss' % str(max_age), | 444 for issue in issues] |
392 user_filter] | 445 else: |
393 [stdout, _] = subprocess.Popen(gquery_cmd, stdout=subprocess.PIPE, | 446 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 | 447 |
398 # TODO(cjhopman): should we filter abandoned changes? | 448 # 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) | 449 issues = filter(self.filter_issue, issues) |
401 issues = sorted(issues, key=lambda i: i['modified'], reverse=True) | 450 issues = sorted(issues, key=lambda i: i['modified'], reverse=True) |
402 | 451 |
403 return issues | 452 return issues |
404 | 453 |
405 def process_gerrit_issue(self, instance, issue): | 454 def process_gerrit_ssh_issue(self, instance, issue): |
406 ret = {} | 455 ret = {} |
407 ret['review_url'] = issue['url'] | 456 ret['review_url'] = issue['url'] |
408 if 'shorturl' in instance: | 457 if 'shorturl' in instance: |
409 ret['review_url'] = 'http://%s/%s' % (instance['shorturl'], | 458 ret['review_url'] = 'http://%s/%s' % (instance['shorturl'], |
410 issue['number']) | 459 issue['number']) |
411 ret['header'] = issue['subject'] | 460 ret['header'] = issue['subject'] |
412 ret['owner'] = issue['owner']['email'] | 461 ret['owner'] = issue['owner']['email'] |
413 ret['author'] = ret['owner'] | 462 ret['author'] = ret['owner'] |
414 ret['created'] = datetime.fromtimestamp(issue['createdOn']) | 463 ret['created'] = datetime.fromtimestamp(issue['createdOn']) |
415 ret['modified'] = datetime.fromtimestamp(issue['lastUpdated']) | 464 ret['modified'] = datetime.fromtimestamp(issue['lastUpdated']) |
416 if 'comments' in issue: | 465 if 'comments' in issue: |
417 ret['replies'] = self.process_gerrit_issue_replies(issue['comments']) | 466 ret['replies'] = self.process_gerrit_ssh_issue_replies(issue['comments']) |
418 else: | 467 else: |
419 ret['replies'] = [] | 468 ret['replies'] = [] |
420 ret['reviewers'] = set() | 469 ret['reviewers'] = set(r['author'] for r in ret['replies']) |
421 for reply in ret['replies']: | 470 ret['reviewers'].discard(ret['author']) |
422 if reply['author'] != ret['author']: | |
423 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 ret.append({ |
432 r['author'] = reply['reviewer']['email'] | 479 'author': reply['reviewer']['email'], |
433 r['created'] = datetime.fromtimestamp(reply['timestamp']) | 480 'created': datetime.fromtimestamp(reply['timestamp']), |
434 r['content'] = '' | 481 'content': '', |
435 ret.append(r) | 482 }) |
| 483 return ret |
| 484 |
| 485 def process_gerrit_rest_issue(self, instance, issue): |
| 486 ret = {} |
| 487 ret['review_url'] = 'https://%s/%s' % (instance['url'], issue['_number']) |
| 488 if 'shorturl' in instance: |
| 489 # TODO(deymo): Move this short link to https once crosreview.com supports |
| 490 # it. |
| 491 ret['review_url'] = 'http://%s/%s' % (instance['shorturl'], |
| 492 issue['_number']) |
| 493 ret['header'] = issue['subject'] |
| 494 ret['owner'] = issue['owner']['email'] |
| 495 ret['author'] = ret['owner'] |
| 496 ret['created'] = datetime_from_gerrit(issue['created']) |
| 497 ret['modified'] = datetime_from_gerrit(issue['updated']) |
| 498 if 'messages' in issue: |
| 499 ret['replies'] = self.process_gerrit_rest_issue_replies(issue['messages']) |
| 500 else: |
| 501 ret['replies'] = [] |
| 502 ret['reviewers'] = set(r['author'] for r in ret['replies']) |
| 503 ret['reviewers'].discard(ret['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 ret.append({ |
| 512 'author': reply['author']['email'], |
| 513 'created': datetime_from_gerrit(reply['date']), |
| 514 'content': reply['message'], |
| 515 }) |
436 return ret | 516 return ret |
437 | 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 |
(...skipping 579 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 |