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

Side by Side Diff: my_activity.py

Issue 137373004: Remove WebKit bugzilla/git support from my_activity.py (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Created 6 years, 11 months 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
25 import datetime 24 import datetime
26 from datetime import datetime 25 from datetime import datetime
27 from datetime import timedelta 26 from datetime import timedelta
28 from functools import partial 27 from functools import partial
29 import json 28 import json
30 import optparse 29 import optparse
31 import os 30 import os
32 import re
33 import subprocess 31 import subprocess
34 import sys 32 import sys
35 import urllib 33 import urllib
36 import urllib2 34 import urllib2
37 35
38 import gerrit_util 36 import gerrit_util
39 import rietveld 37 import rietveld
40 from third_party import upload 38 from third_party import upload
41 39
42 # Imported later, once options are set.
43 webkitpy = None
44
45 try: 40 try:
46 from dateutil.relativedelta import relativedelta # pylint: disable=F0401 41 from dateutil.relativedelta import relativedelta # pylint: disable=F0401
47 except ImportError: 42 except ImportError:
48 print 'python-dateutil package required' 43 print 'python-dateutil package required'
49 exit(1) 44 exit(1)
50 45
51 # python-keyring provides easy access to the system keyring. 46 # python-keyring provides easy access to the system keyring.
52 try: 47 try:
53 import keyring # pylint: disable=W0611,F0401 48 import keyring # pylint: disable=W0611,F0401
54 except ImportError: 49 except ImportError:
55 print 'Consider installing python-keyring' 50 print 'Consider installing python-keyring'
56 51
57 def webkit_account(user):
58 if not webkitpy:
59 return None
60 committer_list = webkitpy.common.config.committers.CommitterList()
61 email = user + "@chromium.org"
62 return committer_list.account_by_email(email)
63
64 def user_to_webkit_email(user):
65 account = webkit_account(user)
66 if not account:
67 return None
68 return account.emails[0]
69
70 def user_to_webkit_owner_search(user):
71 account = webkit_account(user)
72 if not account:
73 return ['--author=%s@chromium.org' % user]
74 search = []
75 for email in account.emails:
76 search.append('--author=' + email)
77 # commit-bot is author for contributors who are not committers.
78 search.append('--grep=Patch by ' + account.full_name)
79 return search
80
81 def user_to_webkit_reviewer_search(user):
82 committer_list = webkitpy.common.config.committers.CommitterList()
83 email = user + "@chromium.org"
84 account = committer_list.reviewer_by_email(email)
85 if not account:
86 return []
87 return ['--grep=Reviewed by ' + account.full_name]
88
89 rietveld_instances = [ 52 rietveld_instances = [
90 { 53 {
91 'url': 'codereview.chromium.org', 54 'url': 'codereview.chromium.org',
92 'shorturl': 'crrev.com', 55 'shorturl': 'crrev.com',
93 'supports_owner_modified_query': True, 56 'supports_owner_modified_query': True,
94 'requires_auth': False, 57 'requires_auth': False,
95 'email_domain': 'chromium.org', 58 'email_domain': 'chromium.org',
96 }, 59 },
97 { 60 {
98 'url': 'chromereviews.googleplex.com', 61 'url': 'chromereviews.googleplex.com',
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after
150 'name': 'google-breakpad', 113 'name': 'google-breakpad',
151 }, 114 },
152 { 115 {
153 'name': 'gyp', 116 'name': 'gyp',
154 }, 117 },
155 { 118 {
156 'name': 'skia', 119 'name': 'skia',
157 }, 120 },
158 ] 121 ]
159 122
160 bugzilla_instances = [
161 {
162 'search_url': 'http://bugs.webkit.org/buglist.cgi',
163 'url': 'wkb.ug',
164 'user_func': user_to_webkit_email,
165 },
166 ]
167
168 git_instances = [
169 {
170 'option': 'webkit_repo',
171 'change_re':
172 r'git-svn-id: http://svn\.webkit\.org/repository/webkit/trunk@(\d*)',
173 'change_url': 'trac.webkit.org/changeset',
174 'review_re': r'https://bugs\.webkit\.org/show_bug\.cgi\?id\=(\d*)',
175 'review_url': 'wkb.ug',
176 'review_prop': 'webkit_bug_id',
177
178 'owner_search_func': user_to_webkit_owner_search,
179 'reviewer_search_func': user_to_webkit_reviewer_search,
180 },
181 ]
182
183 # Uses ClientLogin to authenticate the user for Google Code issue trackers. 123 # Uses ClientLogin to authenticate the user for Google Code issue trackers.
184 def get_auth_token(email): 124 def get_auth_token(email):
185 # KeyringCreds will use the system keyring on the first try, and prompt for 125 # KeyringCreds will use the system keyring on the first try, and prompt for
186 # a password on the next ones. 126 # a password on the next ones.
187 creds = upload.KeyringCreds('code.google.com', 'code.google.com', email) 127 creds = upload.KeyringCreds('code.google.com', 'code.google.com', email)
188 for _ in xrange(3): 128 for _ in xrange(3):
189 email, password = creds.GetUserCredentials() 129 email, password = creds.GetUserCredentials()
190 url = 'https://www.google.com/accounts/ClientLogin' 130 url = 'https://www.google.com/accounts/ClientLogin'
191 data = urllib.urlencode({ 131 data = urllib.urlencode({
192 'Email': email, 132 'Email': email,
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after
262 def __init__(self, options): 202 def __init__(self, options):
263 self.options = options 203 self.options = options
264 self.modified_after = options.begin 204 self.modified_after = options.begin
265 self.modified_before = options.end 205 self.modified_before = options.end
266 self.user = options.user 206 self.user = options.user
267 self.changes = [] 207 self.changes = []
268 self.reviews = [] 208 self.reviews = []
269 self.issues = [] 209 self.issues = []
270 self.check_cookies() 210 self.check_cookies()
271 self.google_code_auth_token = None 211 self.google_code_auth_token = None
272 self.webkit_repo = options.webkit_repo
273 if self.webkit_repo:
274 self.setup_webkit_info()
275 212
276 # Check the codereview cookie jar to determine which Rietveld instances to 213 # Check the codereview cookie jar to determine which Rietveld instances to
277 # authenticate to. 214 # authenticate to.
278 def check_cookies(self): 215 def check_cookies(self):
279 cookie_file = os.path.expanduser('~/.codereview_upload_cookies') 216 cookie_file = os.path.expanduser('~/.codereview_upload_cookies')
280 if not os.path.exists(cookie_file): 217 if not os.path.exists(cookie_file):
281 print 'No Rietveld cookie file found.' 218 print 'No Rietveld cookie file found.'
282 cookie_jar = [] 219 cookie_jar = []
283 else: 220 else:
284 cookie_jar = cookielib.MozillaCookieJar(cookie_file) 221 cookie_jar = cookielib.MozillaCookieJar(cookie_file)
(...skipping 316 matching lines...) Expand 10 before | Expand all | Expand 10 after
601 538
602 ret = [] 539 ret = []
603 for entry in replies['feed']['entry']: 540 for entry in replies['feed']['entry']:
604 e = {} 541 e = {}
605 e['created'] = datetime_from_google_code(entry['published']['$t']) 542 e['created'] = datetime_from_google_code(entry['published']['$t'])
606 e['content'] = entry['content']['$t'] 543 e['content'] = entry['content']['$t']
607 e['author'] = entry['author'][0]['name']['$t'] 544 e['author'] = entry['author'][0]['name']['$t']
608 ret.append(e) 545 ret.append(e)
609 return ret 546 return ret
610 547
611 @staticmethod
612 def git_cmd(repo, *args):
613 cmd = ['git', '--git-dir=%s/.git' % repo]
614 cmd.extend(args)
615 [stdout, _] = subprocess.Popen(cmd, stdout=subprocess.PIPE,
616 stderr=subprocess.PIPE).communicate()
617 lines = str(stdout).split('\n')[:-1]
618 return lines
619
620 def git_search(self, instance, owner=None, reviewer=None):
621 repo = getattr(self, instance['option'])
622 if not repo:
623 return []
624
625 search = []
626 if owner:
627 search.extend(instance['owner_search_func'](owner))
628 if reviewer:
629 search.extend(instance['reviewer_search_func'](reviewer))
630 if not len(search):
631 return []
632
633 self.git_cmd(repo, 'fetch', 'origin')
634
635 time_format = '%Y-%m-%d %H:%M:%S'
636 log_args = [
637 '--after=' + self.modified_after.strftime(time_format),
638 '--before=' + self.modified_before.strftime(time_format),
639 '--format=%H'
640 ]
641 commits = set()
642 for query in search:
643 query_args = [query]
644 query_args.extend(log_args)
645 commits |= set(self.git_cmd(repo, 'log', 'origin/master', *query_args))
646
647 ret = []
648 for commit in commits:
649 output = self.git_cmd(repo, 'log', commit + "^!", "--format=%cn%n%cd%n%B")
650 author = output[0]
651 date = datetime.strptime(output[1], "%a %b %d %H:%M:%S %Y +0000")
652 processed = self.process_git_commit(instance, author, date, output[2:])
653 if processed:
654 ret.append(processed)
655
656 ret = sorted(ret, key=lambda i: i['modified'], reverse=True)
657 return ret
658
659 @staticmethod
660 def process_git_commit(instance, author, date, log):
661 ret = {}
662 ret['owner'] = author
663 ret['author'] = author
664 ret['modified'] = date
665 ret['created'] = date
666 ret['header'] = log[0]
667
668 reviews = []
669 reviewers = []
670 changes = []
671
672 for line in log:
673 match = re.match(r'Reviewed by ([^.]*)', line)
674 if match:
675 reviewers.append(match.group(1))
676 if instance['review_re']:
677 match = re.match(instance['review_re'], line)
678 if match:
679 reviews.append(int(match.group(1)))
680 if instance['change_re']:
681 match = re.match(instance['change_re'], line)
682 if match:
683 changes.append(int(match.group(1)))
684
685 committer_list = webkitpy.common.config.committers.CommitterList()
686 ret['reviewers'] = set(
687 (committer_list.contributor_by_name(r).emails[0] for r in reviewers))
688
689 # Reviews more useful than change link itself, but tricky if multiple
690 # Reviews == bugs for WebKit changes
691 if len(reviews) == 1:
692 url = 'http://%s/%d' % (instance['review_url'], reviews[0])
693 if instance['review_prop']:
694 ret[instance['review_prop']] = reviews[0]
695 elif len(changes) == 1:
696 url = 'http://%s/%d' % (instance['change_url'], changes[0])
697 else:
698 # Couldn't find anything.
699 return None
700 ret['review_url'] = url
701
702 return ret
703
704 def bugzilla_issues(self, instance, user):
705 if instance['user_func']:
706 user = instance['user_func'](user)
707 if not user:
708 return []
709
710 # This search is a little iffy, as it returns any bug that has been
711 # modified over a time period in any way and that a user has ever commented
712 # on, but that's the best that Bugzilla can get us. Oops.
713 commented = { 'emaillongdesc1': 1 }
714 issues = self.bugzilla_search(instance, user, commented)
715 issues = filter(lambda issue: issue['owner'] != user, issues)
716
717 reported = { 'emailreporter1': 1, 'chfield': '[Bug creation]' }
718 issues.extend(self.bugzilla_search(instance, user, reported))
719
720 # Remove duplicates by bug id
721 seen = {}
722 pruned = []
723 for issue in issues:
724 bug_id = issue['webkit_bug_id']
725 if bug_id in seen:
726 continue
727 seen[bug_id] = True
728 pruned.append(issue)
729
730 # Bugzilla has no modified time, so sort by id?
731 pruned = sorted(pruned, key=lambda i: i['webkit_bug_id'])
732 return issues
733
734 def bugzilla_search(self, instance, user, params):
735 time_format = '%Y-%m-%d'
736 values = {
737 'chfieldfrom': self.modified_after.strftime(time_format),
738 'chfieldto': self.modified_before.strftime(time_format),
739 'ctype': 'csv',
740 'emailtype1': 'substring',
741 'email1': '%s' % user,
742 }
743 values.update(params)
744
745 # Must be GET not POST
746 data = urllib.urlencode(values)
747 req = urllib2.Request("%s?%s" % (instance['search_url'], data))
748 response = urllib2.urlopen(req)
749 reader = csv.reader(response)
750 reader.next() # skip the header line
751
752 issues = map(partial(self.process_bugzilla_issue, instance), reader)
753 return issues
754
755 @staticmethod
756 def process_bugzilla_issue(instance, issue):
757 bug_id, owner, desc = int(issue[0]), issue[4], issue[7]
758
759 ret = {}
760 ret['owner'] = owner
761 ret['author'] = owner
762 ret['review_url'] = 'http://%s/%d' % (instance['url'], bug_id)
763 ret['url'] = ret['review_url']
764 ret['header'] = desc
765 ret['webkit_bug_id'] = bug_id
766 return ret
767
768 def setup_webkit_info(self):
769 assert(self.webkit_repo)
770 git_dir = os.path.normpath(self.webkit_repo + "/.git")
771 if not os.path.exists(git_dir):
772 print "%s doesn't exist, skipping WebKit checks." % git_dir
773 self.webkit_repo = None
774 return
775
776 try:
777 self.git_cmd(self.webkit_repo, "fetch", "origin")
778 except subprocess.CalledProcessError:
779 print "Failed to update WebKit repo, skipping WebKit checks."
780 self.webkit_repo = None
781 return
782
783 path = "Tools/Scripts"
784 full_path = os.path.normpath("%s/%s" % (self.options.webkit_repo, path))
785 sys.path.append(full_path)
786
787 try:
788 global webkitpy
789 webkitpy = __import__('webkitpy.common.config.committers')
790 except ImportError:
791 print "Failed to import WebKit committer list, skipping WebKit checks."
792 self.webkit_repo = None
793 return
794
795 if not webkit_account(self.user):
796 email = self.user + "@chromium.org"
797 print "No %s in committers.py, skipping WebKit checks." % email
798 self.webkit_repo = None
799
800 def print_heading(self, heading): 548 def print_heading(self, heading):
801 print 549 print
802 print self.options.output_format_heading.format(heading=heading) 550 print self.options.output_format_heading.format(heading=heading)
803 551
804 def print_change(self, change): 552 def print_change(self, change):
805 optional_values = { 553 optional_values = {
806 'reviewers': ', '.join(change['reviewers']) 554 'reviewers': ', '.join(change['reviewers'])
807 } 555 }
808 self.print_generic(self.options.output_format, 556 self.print_generic(self.options.output_format,
809 self.options.output_format_changes, 557 self.options.output_format_changes,
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after
886 self.google_code_auth_token = ( 634 self.google_code_auth_token = (
887 get_auth_token(self.options.local_user + '@chromium.org')) 635 get_auth_token(self.options.local_user + '@chromium.org'))
888 636
889 def get_changes(self): 637 def get_changes(self):
890 for instance in rietveld_instances: 638 for instance in rietveld_instances:
891 self.changes += self.rietveld_search(instance, owner=self.user) 639 self.changes += self.rietveld_search(instance, owner=self.user)
892 640
893 for instance in gerrit_instances: 641 for instance in gerrit_instances:
894 self.changes += self.gerrit_search(instance, owner=self.user) 642 self.changes += self.gerrit_search(instance, owner=self.user)
895 643
896 for instance in git_instances:
897 self.changes += self.git_search(instance, owner=self.user)
898
899 def print_changes(self): 644 def print_changes(self):
900 if self.changes: 645 if self.changes:
901 self.print_heading('Changes') 646 self.print_heading('Changes')
902 for change in self.changes: 647 for change in self.changes:
903 self.print_change(change) 648 self.print_change(change)
904 649
905 def get_reviews(self): 650 def get_reviews(self):
906 for instance in rietveld_instances: 651 for instance in rietveld_instances:
907 self.reviews += self.rietveld_search(instance, reviewer=self.user) 652 self.reviews += self.rietveld_search(instance, reviewer=self.user)
908 653
909 for instance in gerrit_instances: 654 for instance in gerrit_instances:
910 reviews = self.gerrit_search(instance, reviewer=self.user) 655 reviews = self.gerrit_search(instance, reviewer=self.user)
911 reviews = filter(lambda r: not username(r['owner']) == self.user, reviews) 656 reviews = filter(lambda r: not username(r['owner']) == self.user, reviews)
912 self.reviews += reviews 657 self.reviews += reviews
913 658
914 for instance in git_instances:
915 self.reviews += self.git_search(instance, reviewer=self.user)
916
917 def print_reviews(self): 659 def print_reviews(self):
918 if self.reviews: 660 if self.reviews:
919 self.print_heading('Reviews') 661 self.print_heading('Reviews')
920 for review in self.reviews: 662 for review in self.reviews:
921 self.print_review(review) 663 self.print_review(review)
922 664
923 def get_issues(self): 665 def get_issues(self):
924 for project in google_code_projects: 666 for project in google_code_projects:
925 self.issues += self.google_code_issue_search(project) 667 self.issues += self.google_code_issue_search(project)
926 668
927 for instance in bugzilla_instances:
928 self.issues += self.bugzilla_issues(instance, self.user)
929
930 def print_issues(self): 669 def print_issues(self):
931 if self.issues: 670 if self.issues:
932 self.print_heading('Issues') 671 self.print_heading('Issues')
933 for issue in self.issues: 672 for issue in self.issues:
934 self.print_issue(issue) 673 self.print_issue(issue)
935 674
936 def process_activities(self):
937 # If a webkit bug was a review, don't list it as an issue.
938 ids = {}
939 for review in self.reviews + self.changes:
940 if 'webkit_bug_id' in review:
941 ids[review['webkit_bug_id']] = True
942
943 def duplicate_issue(issue):
944 if 'webkit_bug_id' not in issue:
945 return False
946 return issue['webkit_bug_id'] in ids
947
948 self.issues = filter(lambda issue: not duplicate_issue(issue), self.issues)
949
950 def print_activity(self): 675 def print_activity(self):
951 self.print_changes() 676 self.print_changes()
952 self.print_reviews() 677 self.print_reviews()
953 self.print_issues() 678 self.print_issues()
954 679
955 680
956 def main(): 681 def main():
957 # Silence upload.py. 682 # Silence upload.py.
958 rietveld.upload.verbosity = 0 683 rietveld.upload.verbosity = 0
959 684
960 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__) 685 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
961 parser.add_option( 686 parser.add_option(
962 '-u', '--user', metavar='<email>', 687 '-u', '--user', metavar='<email>',
963 default=os.environ.get('USER'), 688 default=os.environ.get('USER'),
964 help='Filter on user, default=%default') 689 help='Filter on user, default=%default')
965 parser.add_option( 690 parser.add_option(
966 '--webkit_repo', metavar='<dir>',
967 default='%s' % os.environ.get('WEBKIT_DIR'),
968 help='Local path to WebKit repository, default=%default')
969 parser.add_option(
970 '-b', '--begin', metavar='<date>', 691 '-b', '--begin', metavar='<date>',
971 help='Filter issues created after the date') 692 help='Filter issues created after the date')
972 parser.add_option( 693 parser.add_option(
973 '-e', '--end', metavar='<date>', 694 '-e', '--end', metavar='<date>',
974 help='Filter issues created before the date') 695 help='Filter issues created before the date')
975 quarter_begin, quarter_end = get_quarter_of(datetime.today() - 696 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
976 relativedelta(months=2)) 697 relativedelta(months=2))
977 parser.add_option( 698 parser.add_option(
978 '-Q', '--last_quarter', action='store_true', 699 '-Q', '--last_quarter', action='store_true',
979 help='Use last quarter\'s dates, e.g. %s to %s' % ( 700 help='Use last quarter\'s dates, e.g. %s to %s' % (
(...skipping 120 matching lines...) Expand 10 before | Expand all | Expand 10 after
1100 821
1101 print 'Looking up activity.....' 822 print 'Looking up activity.....'
1102 823
1103 if options.changes: 824 if options.changes:
1104 my_activity.get_changes() 825 my_activity.get_changes()
1105 if options.reviews: 826 if options.reviews:
1106 my_activity.get_reviews() 827 my_activity.get_reviews()
1107 if options.issues: 828 if options.issues:
1108 my_activity.get_issues() 829 my_activity.get_issues()
1109 830
1110 my_activity.process_activities()
1111
1112 print '\n\n\n' 831 print '\n\n\n'
1113 832
1114 my_activity.print_changes() 833 my_activity.print_changes()
1115 my_activity.print_reviews() 834 my_activity.print_reviews()
1116 my_activity.print_issues() 835 my_activity.print_issues()
1117 return 0 836 return 0
1118 837
1119 838
1120 if __name__ == '__main__': 839 if __name__ == '__main__':
1121 sys.exit(main()) 840 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