Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(309)

Side by Side Diff: my_activity.py

Issue 11365078: Add WebKit bugzilla/git search to my_activity.py (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Address review comments Created 8 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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.
11 - my_activity.py -Y for stats for this year. 11 - my_activity.py -Y for stats for this year.
12 - my_activity.py -b 4/5/12 for stats since 4/5/12. 12 - my_activity.py -b 4/5/12 for stats since 4/5/12.
13 - my_activity.py -b 4/5/12 -e 6/7/12 for stats between 4/5/12 and 6/7/12. 13 - my_activity.py -b 4/5/12 -e 6/7/12 for stats between 4/5/12 and 6/7/12.
14 """ 14 """
15 15
16 # These services typically only provide a created time and a last modified time 16 # These services typically only provide a created time and a last modified time
17 # for each item for general queries. This is not enough to determine if there 17 # for each item for general queries. This is not enough to determine if there
18 # was activity in a given time period. So, we first query for all things created 18 # was activity in a given time period. So, we first query for all things created
19 # before end and modified after begin. Then, we get the details of each item and 19 # before end and modified after begin. Then, we get the details of each item and
20 # check those details to determine if there was activity in the given period. 20 # check those details to determine if there was activity in the given period.
21 # This means that query time scales mostly with (today() - begin). 21 # This means that query time scales mostly with (today() - begin).
22 22
23 import cookielib 23 import cookielib
24 import csv
24 import datetime 25 import datetime
25 from datetime import datetime 26 from datetime import datetime
26 from datetime import timedelta 27 from datetime import timedelta
27 from functools import partial 28 from functools import partial
28 import json 29 import json
29 import optparse 30 import optparse
30 import os 31 import os
32 import re
31 import subprocess 33 import subprocess
32 import sys 34 import sys
33 import urllib 35 import urllib
34 import urllib2 36 import urllib2
35 37
36 import rietveld 38 import rietveld
37 from third_party import upload 39 from third_party import upload
38 40
41 # Imported later, once options are set.
42 webkitpy = None
43
39 try: 44 try:
40 from dateutil.relativedelta import relativedelta # pylint: disable=F0401 45 from dateutil.relativedelta import relativedelta # pylint: disable=F0401
41 except ImportError: 46 except ImportError:
42 print 'python-dateutil package required' 47 print 'python-dateutil package required'
43 exit(1) 48 exit(1)
44 49
45 # python-keyring provides easy access to the system keyring. 50 # python-keyring provides easy access to the system keyring.
46 try: 51 try:
47 import keyring # pylint: disable=W0611,F0401 52 import keyring # pylint: disable=W0611,F0401
48 except ImportError: 53 except ImportError:
49 print 'Consider installing python-keyring' 54 print 'Consider installing python-keyring'
50 55
56 def webkit_account(user):
57 if not webkitpy:
58 return None
59 committer_list = webkitpy.common.config.committers.CommitterList()
60 email = user + "@chromium.org"
61 return committer_list.account_by_email(email)
62
63 def user_to_webkit_email(user):
64 account = webkit_account(user)
65 if not account:
66 return None
67 return account.emails[0]
68
69 def user_to_webkit_owner_search(user):
70 account = webkit_account(user)
71 if not account:
72 return ['--author=%s@chromium.org' % user]
73 search = []
74 for email in account.emails:
75 search.append('--author=' + email)
76 # commit-bot is author for contributors who are not committers.
77 search.append('--grep=Patch by ' + account.full_name)
78 return search
79
80 def user_to_webkit_reviewer_search(user):
81 committer_list = webkitpy.common.config.committers.CommitterList()
82 email = user + "@chromium.org"
83 account = committer_list.reviewer_by_email(email)
84 if not account:
85 return []
86 return ['--grep=Reviewed by ' + account.full_name]
51 87
52 rietveld_instances = [ 88 rietveld_instances = [
53 { 89 {
54 'url': 'codereview.chromium.org', 90 'url': 'codereview.chromium.org',
55 'shorturl': 'crrev.com', 91 'shorturl': 'crrev.com',
56 'supports_owner_modified_query': True, 92 'supports_owner_modified_query': True,
57 'requires_auth': False, 93 'requires_auth': False,
58 'email_domain': 'chromium.org', 94 'email_domain': 'chromium.org',
59 }, 95 },
60 { 96 {
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after
101 'name': 'chrome-os-partner', 137 'name': 'chrome-os-partner',
102 }, 138 },
103 { 139 {
104 'name': 'google-breakpad', 140 'name': 'google-breakpad',
105 }, 141 },
106 { 142 {
107 'name': 'gyp', 143 'name': 'gyp',
108 } 144 }
109 ] 145 ]
110 146
147 bugzilla_instances = [
148 {
149 'search_url': 'http://bugs.webkit.org/buglist.cgi',
150 'url': 'wkb.ug',
151 'user_func': user_to_webkit_email,
152 },
153 ]
154
155 git_instances = [
156 {
157 'option': 'webkit_repo',
158 'change_re':
159 r'git-svn-id: http://svn\.webkit\.org/repository/webkit/trunk@(\d*)',
160 'change_url': 'trac.webkit.org/changeset',
161 'review_re': r'https://bugs\.webkit\.org/show_bug\.cgi\?id\=(\d*)',
162 'review_url': 'wkb.ug',
163 'review_prop': 'webkit_bug_id',
164
165 'owner_search_func': user_to_webkit_owner_search,
166 'reviewer_search_func': user_to_webkit_reviewer_search,
167 },
168 ]
111 169
112 # Uses ClientLogin to authenticate the user for Google Code issue trackers. 170 # Uses ClientLogin to authenticate the user for Google Code issue trackers.
113 def get_auth_token(email): 171 def get_auth_token(email):
114 # KeyringCreds will use the system keyring on the first try, and prompt for 172 # KeyringCreds will use the system keyring on the first try, and prompt for
115 # a password on the next ones. 173 # a password on the next ones.
116 creds = upload.KeyringCreds('code.google.com', 'code.google.com', email) 174 creds = upload.KeyringCreds('code.google.com', 'code.google.com', email)
117 for _ in xrange(3): 175 for _ in xrange(3):
118 email, password = creds.GetUserCredentials() 176 email, password = creds.GetUserCredentials()
119 url = 'https://www.google.com/accounts/ClientLogin' 177 url = 'https://www.google.com/accounts/ClientLogin'
120 data = urllib.urlencode({ 178 data = urllib.urlencode({
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after
187 def __init__(self, options): 245 def __init__(self, options):
188 self.options = options 246 self.options = options
189 self.modified_after = options.begin 247 self.modified_after = options.begin
190 self.modified_before = options.end 248 self.modified_before = options.end
191 self.user = options.user 249 self.user = options.user
192 self.changes = [] 250 self.changes = []
193 self.reviews = [] 251 self.reviews = []
194 self.issues = [] 252 self.issues = []
195 self.check_cookies() 253 self.check_cookies()
196 self.google_code_auth_token = None 254 self.google_code_auth_token = None
255 self.webkit_repo = options.webkit_repo
256 if self.webkit_repo:
257 self.setup_webkit_info()
197 258
198 # Check the codereview cookie jar to determine which Rietveld instances to 259 # Check the codereview cookie jar to determine which Rietveld instances to
199 # authenticate to. 260 # authenticate to.
200 def check_cookies(self): 261 def check_cookies(self):
201 cookie_file = os.path.expanduser('~/.codereview_upload_cookies') 262 cookie_file = os.path.expanduser('~/.codereview_upload_cookies')
202 cookie_jar = cookielib.MozillaCookieJar(cookie_file) 263 cookie_jar = cookielib.MozillaCookieJar(cookie_file)
203 if not os.path.exists(cookie_file): 264 if not os.path.exists(cookie_file):
204 exit(1) 265 exit(1)
205 266
206 try: 267 try:
(...skipping 248 matching lines...) Expand 10 before | Expand all | Expand 10 after
455 ret = [] 516 ret = []
456 for entry in replies['feed']['entry']: 517 for entry in replies['feed']['entry']:
457 e = {} 518 e = {}
458 e['created'] = datetime_from_google_code(entry['published']['$t']) 519 e['created'] = datetime_from_google_code(entry['published']['$t'])
459 e['content'] = entry['content']['$t'] 520 e['content'] = entry['content']['$t']
460 e['author'] = entry['author'][0]['name']['$t'] 521 e['author'] = entry['author'][0]['name']['$t']
461 ret.append(e) 522 ret.append(e)
462 return ret 523 return ret
463 524
464 @staticmethod 525 @staticmethod
526 def git_cmd(repo, *args):
527 cmd = ['git', '--git-dir=%s/.git' % repo]
528 cmd.extend(args)
529 [stdout, _] = subprocess.Popen(cmd, stdout=subprocess.PIPE,
530 stderr=subprocess.PIPE).communicate()
531 lines = str(stdout).split('\n')[:-1]
532 return lines
533
534 def git_search(self, instance, owner=None, reviewer=None):
535 repo = getattr(self, instance['option'])
536 if not repo:
537 return []
538
539 search = []
540 if owner:
541 search.extend(instance['owner_search_func'](owner))
542 if reviewer:
543 search.extend(instance['reviewer_search_func'](reviewer))
544 if not len(search):
545 return []
546
547 self.git_cmd(repo, 'fetch', 'origin')
548
549 time_format = '%Y-%m-%d %H:%M:%S'
550 log_args = [
551 '--after=' + self.modified_after.strftime(time_format),
552 '--before=' + self.modified_before.strftime(time_format),
553 '--format=%H'
554 ]
555 commits = set()
556 for query in search:
557 query_args = [query]
558 query_args.extend(log_args)
559 commits |= set(self.git_cmd(repo, 'log', 'origin/master', *query_args))
560
561 ret = []
562 for commit in commits:
563 output = self.git_cmd(repo, 'log', commit + "^!", "--format=%cn%n%cd%n%B")
564 author = output[0]
565 date = datetime.strptime(output[1], "%a %b %d %H:%M:%S %Y +0000")
566 ret.append(self.process_git_commit(instance, author, date, output[2:]))
567
568 ret = sorted(ret, key=lambda i: i['modified'], reverse=True)
569 return ret
570
571 @staticmethod
572 def process_git_commit(instance, author, date, log):
573 ret = {}
574 ret['owner'] = author
575 ret['author'] = author
576 ret['modified'] = date
577 ret['created'] = date
578 ret['header'] = log[0]
579
580 reviews = []
581 reviewers = []
582 changes = []
583
584 for line in log:
585 match = re.match(r'Reviewed by ([^.]*)', line)
586 if match:
587 reviewers.append(match.group(1))
588 if instance['review_re']:
589 match = re.match(instance['review_re'], line)
590 if match:
591 reviews.append(int(match.group(1)))
592 if instance['change_re']:
593 match = re.match(instance['change_re'], line)
594 if match:
595 changes.append(int(match.group(1)))
596
597 # TODO(enne): should convert full names to usernames via CommitterList.
598 ret['reviewers'] = set(reviewers)
599
600 # Reviews more useful than change link itself, but tricky if multiple
601 # Reviews == bugs for WebKit changes
602 if len(reviews) == 1:
603 url = 'http://%s/%d' % (instance['review_url'], reviews[0])
604 if instance['review_prop']:
605 ret[instance['review_prop']] = reviews[0]
606 else:
607 url = 'http://%s/%d' % (instance['change_url'], changes[0])
608 ret['review_url'] = url
609
610 return ret
611
612 def bugzilla_issues(self, instance, user):
613 if instance['user_func']:
614 user = instance['user_func'](user)
615 if not user:
616 return []
617
618 # This search is a little iffy, as it returns any bug that has been
619 # modified over a time period in any way and that a user has ever commented
620 # on, but that's the best that Bugzilla can get us. Oops.
621 commented = { 'emaillongdesc1': 1 }
622 issues = self.bugzilla_search(instance, user, commented)
623 issues = filter(lambda issue: issue['owner'] != user, issues)
624
625 reported = { 'emailreporter1': 1, 'chfield': '[Bug creation]' }
626 issues.extend(self.bugzilla_search(instance, user, reported))
627
628 # Remove duplicates by bug id
629 seen = {}
630 pruned = []
631 for issue in issues:
632 bug_id = issue['webkit_bug_id']
633 if bug_id in seen:
634 continue
635 seen[bug_id] = True
636 pruned.append(issue)
637
638 # Bugzilla has no modified time, so sort by id?
639 pruned = sorted(pruned, key=lambda i: i['webkit_bug_id'])
640 return issues
641
642 def bugzilla_search(self, instance, user, params):
643 time_format = '%Y-%m-%d'
644 values = {
645 'chfieldfrom': self.modified_after.strftime(time_format),
646 'chfieldto': self.modified_before.strftime(time_format),
647 'ctype': 'csv',
648 'emailtype1': 'substring',
649 'email1': '%s' % user,
650 }
651 values.update(params)
652
653 # Must be GET not POST
654 data = urllib.urlencode(values)
655 req = urllib2.Request("%s?%s" % (instance['search_url'], data))
656 response = urllib2.urlopen(req)
657 reader = csv.reader(response)
658 reader.next() # skip the header line
659
660 issues = map(partial(self.process_bugzilla_issue, instance), reader)
661 return issues
662
663 @staticmethod
664 def process_bugzilla_issue(instance, issue):
665 bug_id, owner, desc = int(issue[0]), issue[4], issue[7]
666
667 ret = {}
668 ret['owner'] = owner
669 ret['author'] = owner
670 ret['review_url'] = 'http://%s/%d' % (instance['url'], bug_id)
671 ret['url'] = ret['review_url']
672 ret['header'] = desc
673 ret['webkit_bug_id'] = bug_id
674 return ret
675
676 def setup_webkit_info(self):
677 assert(self.webkit_repo)
678 git_dir = os.path.normpath(self.webkit_repo + "/.git")
679 if not os.path.exists(git_dir):
680 print "%s doesn't exist, turning off WebKit checks." % git_dir
681 self.webkit_repo = None
682 return
683
684 try:
685 self.git_cmd(self.webkit_repo, "fetch", "origin")
686 except subprocess.CalledProcessError:
687 print "Failed to update WebKit repo, turning off WebKit checks."
688 self.webkit_repo = None
689 return
690
691 path = "Tools/Scripts"
692 full_path = os.path.normpath("%s/%s" % (self.options.webkit_repo, path))
693 sys.path.append(full_path)
694
695 try:
696 global webkitpy
697 webkitpy = __import__('webkitpy.common.config.committers')
698 except ImportError:
699 print "Failed to import WebKit committer list, turning off WebKit checks."
700 self.webkit_repo = None
701 return
702
703 if not webkit_account(self.user):
704 email = self.user + "@chromium.org"
705 print "No %s in committers.py, turning off WebKit checks." % email
706 self.webkit_repo = None
707
708 @staticmethod
465 def print_change(change): 709 def print_change(change):
466 print '%s %s' % ( 710 print '%s %s' % (
467 change['review_url'], 711 change['review_url'],
468 change['header'], 712 change['header'],
469 ) 713 )
470 714
471 @staticmethod 715 @staticmethod
472 def print_issue(issue): 716 def print_issue(issue):
473 print '%s %s' % ( 717 print '%s %s' % (
474 issue['url'], 718 issue['url'],
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
512 self.google_code_auth_token = ( 756 self.google_code_auth_token = (
513 get_auth_token(self.options.local_user + '@chromium.org')) 757 get_auth_token(self.options.local_user + '@chromium.org'))
514 758
515 def get_changes(self): 759 def get_changes(self):
516 for instance in rietveld_instances: 760 for instance in rietveld_instances:
517 self.changes += self.rietveld_search(instance, owner=self.user) 761 self.changes += self.rietveld_search(instance, owner=self.user)
518 762
519 for instance in gerrit_instances: 763 for instance in gerrit_instances:
520 self.changes += self.gerrit_search(instance, owner=self.user) 764 self.changes += self.gerrit_search(instance, owner=self.user)
521 765
766 for instance in git_instances:
767 self.changes += self.git_search(instance, owner=self.user)
768
522 def print_changes(self): 769 def print_changes(self):
523 if self.changes: 770 if self.changes:
524 print '\nChanges:' 771 print '\nChanges:'
525 for change in self.changes: 772 for change in self.changes:
526 self.print_change(change) 773 self.print_change(change)
527 774
528 def get_reviews(self): 775 def get_reviews(self):
529 for instance in rietveld_instances: 776 for instance in rietveld_instances:
530 self.reviews += self.rietveld_search(instance, reviewer=self.user) 777 self.reviews += self.rietveld_search(instance, reviewer=self.user)
531 778
532 for instance in gerrit_instances: 779 for instance in gerrit_instances:
533 reviews = self.gerrit_search(instance, reviewer=self.user) 780 reviews = self.gerrit_search(instance, reviewer=self.user)
534 reviews = filter(lambda r: not username(r['owner']) == self.user, reviews) 781 reviews = filter(lambda r: not username(r['owner']) == self.user, reviews)
535 self.reviews += reviews 782 self.reviews += reviews
536 783
784 for instance in git_instances:
785 self.reviews += self.git_search(instance, reviewer=self.user)
786
537 def print_reviews(self): 787 def print_reviews(self):
538 if self.reviews: 788 if self.reviews:
539 print '\nReviews:' 789 print '\nReviews:'
540 for review in self.reviews: 790 for review in self.reviews:
541 self.print_change(review) 791 self.print_change(review)
542 792
543 def get_issues(self): 793 def get_issues(self):
544 for project in google_code_projects: 794 for project in google_code_projects:
545 self.issues += self.google_code_issue_search(project) 795 self.issues += self.google_code_issue_search(project)
546 796
797 for instance in bugzilla_instances:
798 self.issues += self.bugzilla_issues(instance, self.user)
799
800 def process_activities(self):
801 # If a webkit bug was a review, don't list it as an issue.
802 ids = {}
803 for review in self.reviews + self.changes:
804 if 'webkit_bug_id' in review:
805 ids[review['webkit_bug_id']] = True
806
807 def duplicate_issue(issue):
808 if 'webkit_bug_id' not in issue:
809 return False
810 return issue['webkit_bug_id'] in ids
811
812 self.issues = filter(lambda issue: not duplicate_issue(issue), self.issues)
813
547 def print_issues(self): 814 def print_issues(self):
548 if self.issues: 815 if self.issues:
549 print '\nIssues:' 816 print '\nIssues:'
550 for c in self.issues: 817 for c in self.issues:
551 self.print_issue(c) 818 self.print_issue(c)
552 819
553 def print_activity(self): 820 def print_activity(self):
554 self.print_changes() 821 self.print_changes()
555 self.print_reviews() 822 self.print_reviews()
556 self.print_issues() 823 self.print_issues()
557 824
558 825
559 def main(): 826 def main():
560 # Silence upload.py. 827 # Silence upload.py.
561 rietveld.upload.verbosity = 0 828 rietveld.upload.verbosity = 0
562 829
563 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__) 830 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
564 parser.add_option( 831 parser.add_option(
565 '-u', '--user', metavar='<email>', 832 '-u', '--user', metavar='<email>',
566 default=os.environ.get('USER'), 833 default=os.environ.get('USER'),
567 help='Filter on user, default=%default') 834 help='Filter on user, default=%default')
568 parser.add_option( 835 parser.add_option(
836 '--webkit_repo', metavar='<dir>',
837 default='%s' % os.environ.get('WEBKIT_DIR'),
838 help='Local path to WebKit repository, default=%default')
839 parser.add_option(
569 '-b', '--begin', metavar='<date>', 840 '-b', '--begin', metavar='<date>',
570 help='Filter issues created after the date') 841 help='Filter issues created after the date')
571 parser.add_option( 842 parser.add_option(
572 '-e', '--end', metavar='<date>', 843 '-e', '--end', metavar='<date>',
573 help='Filter issues created before the date') 844 help='Filter issues created before the date')
574 quarter_begin, quarter_end = get_quarter_of(datetime.today() - 845 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
575 relativedelta(months=2)) 846 relativedelta(months=2))
576 parser.add_option( 847 parser.add_option(
577 '-Q', '--last_quarter', action='store_true', 848 '-Q', '--last_quarter', action='store_true',
578 help='Use last quarter\'s dates, e.g. %s to %s' % ( 849 help='Use last quarter\'s dates, e.g. %s to %s' % (
(...skipping 78 matching lines...) Expand 10 before | Expand all | Expand 10 after
657 928
658 print 'Looking up activity.....' 929 print 'Looking up activity.....'
659 930
660 if options.changes: 931 if options.changes:
661 my_activity.get_changes() 932 my_activity.get_changes()
662 if options.reviews: 933 if options.reviews:
663 my_activity.get_reviews() 934 my_activity.get_reviews()
664 if options.issues: 935 if options.issues:
665 my_activity.get_issues() 936 my_activity.get_issues()
666 937
938 my_activity.process_activities()
939
667 print '\n\n\n' 940 print '\n\n\n'
668 941
669 my_activity.print_changes() 942 my_activity.print_changes()
670 my_activity.print_reviews() 943 my_activity.print_reviews()
671 my_activity.print_issues() 944 my_activity.print_issues()
672 return 0 945 return 0
673 946
674 947
675 if __name__ == '__main__': 948 if __name__ == '__main__':
676 sys.exit(main()) 949 sys.exit(main())
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698