blob: 17cb4cf83b1f555bc5cfd56497a77bebcbab2ffa [file] [log] [blame]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001#!/usr/bin/env python
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
4# found in the LICENSE file.
5
6"""Get stats about your activity.
7
8Example:
9 - my_activity.py for stats for the current week (last week on mondays).
10 - my_activity.py -Q for stats for last quarter.
11 - my_activity.py -Y for stats for this year.
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.
14"""
15
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000016# TODO(vadimsh): This script knows too much about ClientLogin and cookies. It
17# will stop to work on ~20 Apr 2015.
18
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000019# These services typically only provide a created time and a last modified time
20# for each item for general queries. This is not enough to determine if there
21# was activity in a given time period. So, we first query for all things created
22# before end and modified after begin. Then, we get the details of each item and
23# check those details to determine if there was activity in the given period.
24# This means that query time scales mostly with (today() - begin).
25
26import cookielib
27import datetime
28from datetime import datetime
29from datetime import timedelta
30from functools import partial
31import json
32import optparse
33import os
34import subprocess
35import sys
36import urllib
37import urllib2
38
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000039import auth
stevefung@chromium.org832d51e2015-05-27 12:52:51 +000040import fix_encoding
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000041import gerrit_util
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000042import rietveld
43from third_party import upload
44
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +000045import auth
46from third_party import httplib2
47
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000048try:
49 from dateutil.relativedelta import relativedelta # pylint: disable=F0401
50except ImportError:
51 print 'python-dateutil package required'
52 exit(1)
53
54# python-keyring provides easy access to the system keyring.
55try:
56 import keyring # pylint: disable=W0611,F0401
57except ImportError:
58 print 'Consider installing python-keyring'
59
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000060rietveld_instances = [
61 {
62 'url': 'codereview.chromium.org',
63 'shorturl': 'crrev.com',
64 'supports_owner_modified_query': True,
65 'requires_auth': False,
66 'email_domain': 'chromium.org',
67 },
68 {
69 'url': 'chromereviews.googleplex.com',
70 'shorturl': 'go/chromerev',
71 'supports_owner_modified_query': True,
72 'requires_auth': True,
73 'email_domain': 'google.com',
74 },
75 {
76 'url': 'codereview.appspot.com',
77 'supports_owner_modified_query': True,
78 'requires_auth': False,
79 'email_domain': 'chromium.org',
80 },
81 {
82 'url': 'breakpad.appspot.com',
83 'supports_owner_modified_query': False,
84 'requires_auth': False,
85 'email_domain': 'chromium.org',
86 },
87]
88
89gerrit_instances = [
90 {
deymo@chromium.org6c039202013-09-12 12:28:12 +000091 'url': 'chromium-review.googlesource.com',
deymo@chromium.orge52bd5a2013-08-29 18:05:21 +000092 'shorturl': 'crosreview.com',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000093 },
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000094 {
95 'url': 'chrome-internal-review.googlesource.com',
96 'shorturl': 'crosreview.com/i',
97 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000098]
99
100google_code_projects = [
101 {
deymo@chromium.org69bf3ad2015-02-02 22:19:46 +0000102 'name': 'brillo',
103 'shorturl': 'brbug.com',
104 },
105 {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000106 'name': 'chromium',
107 'shorturl': 'crbug.com',
108 },
109 {
110 'name': 'chromium-os',
deymo@chromium.orgc840e212013-02-13 20:40:22 +0000111 'shorturl': 'crosbug.com',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000112 },
113 {
114 'name': 'chrome-os-partner',
115 },
116 {
117 'name': 'google-breakpad',
118 },
119 {
120 'name': 'gyp',
enne@chromium.orgf01fad32012-11-26 18:09:38 +0000121 },
122 {
123 'name': 'skia',
124 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000125]
126
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000127def username(email):
128 """Keeps the username of an email address."""
129 return email and email.split('@', 1)[0]
130
131
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000132def datetime_to_midnight(date):
133 return date - timedelta(hours=date.hour, minutes=date.minute,
134 seconds=date.second, microseconds=date.microsecond)
135
136
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000137def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000138 begin = (datetime_to_midnight(date) -
139 relativedelta(months=(date.month % 3) - 1, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000140 return begin, begin + relativedelta(months=3)
141
142
143def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000144 begin = (datetime_to_midnight(date) -
145 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000146 return begin, begin + relativedelta(years=1)
147
148
149def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000150 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000151 return begin, begin + timedelta(days=7)
152
153
154def get_yes_or_no(msg):
155 while True:
156 response = raw_input(msg + ' yes/no [no] ')
157 if response == 'y' or response == 'yes':
158 return True
159 elif not response or response == 'n' or response == 'no':
160 return False
161
162
deymo@chromium.org6c039202013-09-12 12:28:12 +0000163def datetime_from_gerrit(date_string):
164 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
165
166
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000167def datetime_from_rietveld(date_string):
deymo@chromium.org29eb6e62014-03-20 01:55:55 +0000168 try:
169 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f')
170 except ValueError:
171 # Sometimes rietveld returns a value without the milliseconds part, so we
172 # attempt to parse those cases as well.
173 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000174
175
176def datetime_from_google_code(date_string):
177 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S.%fZ')
178
179
180class MyActivity(object):
181 def __init__(self, options):
182 self.options = options
183 self.modified_after = options.begin
184 self.modified_before = options.end
185 self.user = options.user
186 self.changes = []
187 self.reviews = []
188 self.issues = []
189 self.check_cookies()
190 self.google_code_auth_token = None
191
192 # Check the codereview cookie jar to determine which Rietveld instances to
193 # authenticate to.
194 def check_cookies(self):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000195 filtered_instances = []
196
197 def has_cookie(instance):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000198 auth_config = auth.extract_auth_config_from_options(self.options)
199 a = auth.get_authenticator_for_host(instance['url'], auth_config)
200 return a.has_cached_credentials()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000201
202 for instance in rietveld_instances:
203 instance['auth'] = has_cookie(instance)
204
205 if filtered_instances:
206 print ('No cookie found for the following Rietveld instance%s:' %
207 ('s' if len(filtered_instances) > 1 else ''))
208 for instance in filtered_instances:
209 print '\t' + instance['url']
210 print 'Use --auth if you would like to authenticate to them.\n'
211
212 def rietveld_search(self, instance, owner=None, reviewer=None):
213 if instance['requires_auth'] and not instance['auth']:
214 return []
215
216
217 email = None if instance['auth'] else ''
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000218 auth_config = auth.extract_auth_config_from_options(self.options)
219 remote = rietveld.Rietveld('https://' + instance['url'], auth_config, email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000220
221 # See def search() in rietveld.py to see all the filters you can use.
222 query_modified_after = None
223
224 if instance['supports_owner_modified_query']:
225 query_modified_after = self.modified_after.strftime('%Y-%m-%d')
226
227 # Rietveld does not allow search by both created_before and modified_after.
228 # (And some instances don't allow search by both owner and modified_after)
229 owner_email = None
230 reviewer_email = None
231 if owner:
232 owner_email = owner + '@' + instance['email_domain']
233 if reviewer:
234 reviewer_email = reviewer + '@' + instance['email_domain']
235 issues = remote.search(
236 owner=owner_email,
237 reviewer=reviewer_email,
238 modified_after=query_modified_after,
239 with_messages=True)
240
241 issues = filter(
242 lambda i: (datetime_from_rietveld(i['created']) < self.modified_before),
243 issues)
244 issues = filter(
245 lambda i: (datetime_from_rietveld(i['modified']) > self.modified_after),
246 issues)
247
248 should_filter_by_user = True
249 issues = map(partial(self.process_rietveld_issue, instance), issues)
250 issues = filter(
251 partial(self.filter_issue, should_filter_by_user=should_filter_by_user),
252 issues)
253 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
254
255 return issues
256
257 def process_rietveld_issue(self, instance, issue):
258 ret = {}
259 ret['owner'] = issue['owner_email']
260 ret['author'] = ret['owner']
261
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000262 ret['reviewers'] = set(issue['reviewers'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000263
264 shorturl = instance['url']
265 if 'shorturl' in instance:
266 shorturl = instance['shorturl']
267
268 ret['review_url'] = 'http://%s/%d' % (shorturl, issue['issue'])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000269
270 # Rietveld sometimes has '\r\n' instead of '\n'.
271 ret['header'] = issue['description'].replace('\r', '').split('\n')[0]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000272
273 ret['modified'] = datetime_from_rietveld(issue['modified'])
274 ret['created'] = datetime_from_rietveld(issue['created'])
275 ret['replies'] = self.process_rietveld_replies(issue['messages'])
276
277 return ret
278
279 @staticmethod
280 def process_rietveld_replies(replies):
281 ret = []
282 for reply in replies:
283 r = {}
284 r['author'] = reply['sender']
285 r['created'] = datetime_from_rietveld(reply['date'])
286 r['content'] = ''
287 ret.append(r)
288 return ret
289
deymo@chromium.org6c039202013-09-12 12:28:12 +0000290 @staticmethod
291 def gerrit_changes_over_ssh(instance, filters):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000292 # See https://review.openstack.org/Documentation/cmd-query.html
293 # Gerrit doesn't allow filtering by created time, only modified time.
deymo@chromium.org6c039202013-09-12 12:28:12 +0000294 gquery_cmd = ['ssh', '-p', str(instance['port']), instance['host'],
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000295 'gerrit', 'query',
296 '--format', 'JSON',
297 '--comments',
deymo@chromium.org6c039202013-09-12 12:28:12 +0000298 '--'] + filters
299 (stdout, _) = subprocess.Popen(gquery_cmd, stdout=subprocess.PIPE,
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000300 stderr=subprocess.PIPE).communicate()
deymo@chromium.org6c039202013-09-12 12:28:12 +0000301 # Drop the last line of the output with the stats.
302 issues = stdout.splitlines()[:-1]
303 return map(json.loads, issues)
304
305 @staticmethod
306 def gerrit_changes_over_rest(instance, filters):
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000307 # Convert the "key:value" filter to a dictionary.
308 req = dict(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000309 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000310 # Instantiate the generator to force all the requests now and catch the
311 # errors here.
312 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
313 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS']))
314 except gerrit_util.GerritError, e:
315 print 'ERROR: Looking up %r: %s' % (instance['url'], e)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000316 return []
317
deymo@chromium.org6c039202013-09-12 12:28:12 +0000318 def gerrit_search(self, instance, owner=None, reviewer=None):
319 max_age = datetime.today() - self.modified_after
320 max_age = max_age.days * 24 * 3600 + max_age.seconds
321 user_filter = 'owner:%s' % owner if owner else 'reviewer:%s' % reviewer
322 filters = ['-age:%ss' % max_age, user_filter]
323
324 # Determine the gerrit interface to use: SSH or REST API:
325 if 'host' in instance:
326 issues = self.gerrit_changes_over_ssh(instance, filters)
327 issues = [self.process_gerrit_ssh_issue(instance, issue)
328 for issue in issues]
329 elif 'url' in instance:
330 issues = self.gerrit_changes_over_rest(instance, filters)
331 issues = [self.process_gerrit_rest_issue(instance, issue)
332 for issue in issues]
333 else:
334 raise Exception('Invalid gerrit_instances configuration.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000335
336 # TODO(cjhopman): should we filter abandoned changes?
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000337 issues = filter(self.filter_issue, issues)
338 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
339
340 return issues
341
deymo@chromium.org6c039202013-09-12 12:28:12 +0000342 def process_gerrit_ssh_issue(self, instance, issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000343 ret = {}
344 ret['review_url'] = issue['url']
deymo@chromium.orge52bd5a2013-08-29 18:05:21 +0000345 if 'shorturl' in instance:
346 ret['review_url'] = 'http://%s/%s' % (instance['shorturl'],
347 issue['number'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000348 ret['header'] = issue['subject']
349 ret['owner'] = issue['owner']['email']
350 ret['author'] = ret['owner']
351 ret['created'] = datetime.fromtimestamp(issue['createdOn'])
352 ret['modified'] = datetime.fromtimestamp(issue['lastUpdated'])
353 if 'comments' in issue:
deymo@chromium.org6c039202013-09-12 12:28:12 +0000354 ret['replies'] = self.process_gerrit_ssh_issue_replies(issue['comments'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000355 else:
356 ret['replies'] = []
deymo@chromium.org6c039202013-09-12 12:28:12 +0000357 ret['reviewers'] = set(r['author'] for r in ret['replies'])
358 ret['reviewers'].discard(ret['author'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000359 return ret
360
361 @staticmethod
deymo@chromium.org6c039202013-09-12 12:28:12 +0000362 def process_gerrit_ssh_issue_replies(replies):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000363 ret = []
364 replies = filter(lambda r: 'email' in r['reviewer'], replies)
365 for reply in replies:
deymo@chromium.org6c039202013-09-12 12:28:12 +0000366 ret.append({
367 'author': reply['reviewer']['email'],
368 'created': datetime.fromtimestamp(reply['timestamp']),
369 'content': '',
370 })
371 return ret
372
373 def process_gerrit_rest_issue(self, instance, issue):
374 ret = {}
375 ret['review_url'] = 'https://%s/%s' % (instance['url'], issue['_number'])
376 if 'shorturl' in instance:
377 # TODO(deymo): Move this short link to https once crosreview.com supports
378 # it.
379 ret['review_url'] = 'http://%s/%s' % (instance['shorturl'],
380 issue['_number'])
381 ret['header'] = issue['subject']
382 ret['owner'] = issue['owner']['email']
383 ret['author'] = ret['owner']
384 ret['created'] = datetime_from_gerrit(issue['created'])
385 ret['modified'] = datetime_from_gerrit(issue['updated'])
386 if 'messages' in issue:
387 ret['replies'] = self.process_gerrit_rest_issue_replies(issue['messages'])
388 else:
389 ret['replies'] = []
390 ret['reviewers'] = set(r['author'] for r in ret['replies'])
391 ret['reviewers'].discard(ret['author'])
392 return ret
393
394 @staticmethod
395 def process_gerrit_rest_issue_replies(replies):
396 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000397 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
398 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000399 for reply in replies:
400 ret.append({
401 'author': reply['author']['email'],
402 'created': datetime_from_gerrit(reply['date']),
403 'content': reply['message'],
404 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000405 return ret
406
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000407 def project_hosting_issue_search(self, instance):
408 auth_config = auth.extract_auth_config_from_options(self.options)
409 authenticator = auth.get_authenticator_for_host(
410 "code.google.com", auth_config)
411 http = authenticator.authorize(httplib2.Http())
412 url = "https://www.googleapis.com/projecthosting/v2/projects/%s/issues" % (
413 instance["name"])
414 epoch = datetime.utcfromtimestamp(0)
415 user_str = '%s@chromium.org' % self.user
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000416
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000417 query_data = urllib.urlencode({
418 'maxResults': 10000,
419 'q': user_str,
420 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
421 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000422 })
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000423 url = url + '?' + query_data
424 _, body = http.request(url)
425 content = json.loads(body)
426 if not content:
427 print "Unable to parse %s response from projecthosting." % (
428 instance["name"])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000429 return []
430
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000431 issues = []
432 if 'items' in content:
433 items = content['items']
434 for item in items:
435 issue = {
436 "header": item["title"],
437 "created": item["published"],
438 "modified": item["updated"],
439 "author": item["author"]["name"],
440 "url": "https://code.google.com/p/%s/issues/detail?id=%s" % (
441 instance["name"], item["id"]),
442 "comments": []
443 }
444 if 'owner' in item:
445 issue['owner'] = item['owner']['name']
446 else:
447 issue['owner'] = 'None'
448 if issue['owner'] == user_str or issue['author'] == user_str:
449 issues.append(issue)
450
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000451 return issues
452
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000453 def print_heading(self, heading):
454 print
455 print self.options.output_format_heading.format(heading=heading)
456
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000457 def print_change(self, change):
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000458 optional_values = {
459 'reviewers': ', '.join(change['reviewers'])
460 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000461 self.print_generic(self.options.output_format,
462 self.options.output_format_changes,
463 change['header'],
464 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000465 change['author'],
466 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000467
468 def print_issue(self, issue):
469 optional_values = {
470 'owner': issue['owner'],
471 }
472 self.print_generic(self.options.output_format,
473 self.options.output_format_issues,
474 issue['header'],
475 issue['url'],
476 issue['author'],
477 optional_values)
478
479 def print_review(self, review):
480 self.print_generic(self.options.output_format,
481 self.options.output_format_reviews,
482 review['header'],
483 review['review_url'],
484 review['author'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000485
486 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000487 def print_generic(default_fmt, specific_fmt,
488 title, url, author,
489 optional_values=None):
490 output_format = specific_fmt if specific_fmt is not None else default_fmt
491 output_format = unicode(output_format)
492 required_values = {
493 'title': title,
494 'url': url,
495 'author': author,
496 }
497 # Merge required and optional values.
498 if optional_values is not None:
499 values = dict(required_values.items() + optional_values.items())
500 else:
501 values = required_values
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000502 print output_format.format(**values).encode(sys.getdefaultencoding())
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000503
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000504
505 def filter_issue(self, issue, should_filter_by_user=True):
506 def maybe_filter_username(email):
507 return not should_filter_by_user or username(email) == self.user
508 if (maybe_filter_username(issue['author']) and
509 self.filter_modified(issue['created'])):
510 return True
511 if (maybe_filter_username(issue['owner']) and
512 (self.filter_modified(issue['created']) or
513 self.filter_modified(issue['modified']))):
514 return True
515 for reply in issue['replies']:
516 if self.filter_modified(reply['created']):
517 if not should_filter_by_user:
518 break
519 if (username(reply['author']) == self.user
520 or (self.user + '@') in reply['content']):
521 break
522 else:
523 return False
524 return True
525
526 def filter_modified(self, modified):
527 return self.modified_after < modified and modified < self.modified_before
528
529 def auth_for_changes(self):
530 #TODO(cjhopman): Move authentication check for getting changes here.
531 pass
532
533 def auth_for_reviews(self):
534 # Reviews use all the same instances as changes so no authentication is
535 # required.
536 pass
537
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000538 def get_changes(self):
539 for instance in rietveld_instances:
540 self.changes += self.rietveld_search(instance, owner=self.user)
541
542 for instance in gerrit_instances:
543 self.changes += self.gerrit_search(instance, owner=self.user)
544
545 def print_changes(self):
546 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000547 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000548 for change in self.changes:
549 self.print_change(change)
550
551 def get_reviews(self):
552 for instance in rietveld_instances:
553 self.reviews += self.rietveld_search(instance, reviewer=self.user)
554
555 for instance in gerrit_instances:
556 reviews = self.gerrit_search(instance, reviewer=self.user)
557 reviews = filter(lambda r: not username(r['owner']) == self.user, reviews)
558 self.reviews += reviews
559
560 def print_reviews(self):
561 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000562 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000563 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000564 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000565
566 def get_issues(self):
567 for project in google_code_projects:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000568 self.issues += self.project_hosting_issue_search(project)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000569
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000570 def print_issues(self):
571 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000572 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000573 for issue in self.issues:
574 self.print_issue(issue)
575
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000576 def print_activity(self):
577 self.print_changes()
578 self.print_reviews()
579 self.print_issues()
580
581
582def main():
583 # Silence upload.py.
584 rietveld.upload.verbosity = 0
585
586 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
587 parser.add_option(
588 '-u', '--user', metavar='<email>',
589 default=os.environ.get('USER'),
590 help='Filter on user, default=%default')
591 parser.add_option(
592 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000593 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000594 parser.add_option(
595 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000596 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000597 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
598 relativedelta(months=2))
599 parser.add_option(
600 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000601 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000602 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
603 parser.add_option(
604 '-Y', '--this_year', action='store_true',
605 help='Use this year\'s dates')
606 parser.add_option(
607 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000608 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000609 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000610 '-W', '--last_week', action='count',
611 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000612 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000613 '-a', '--auth',
614 action='store_true',
615 help='Ask to authenticate for instances with no auth cookie')
616
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000617 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000618 'By default, all activity will be looked up and '
619 'printed. If any of these are specified, only '
620 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000621 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000622 '-c', '--changes',
623 action='store_true',
624 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000625 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000626 '-i', '--issues',
627 action='store_true',
628 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000629 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000630 '-r', '--reviews',
631 action='store_true',
632 help='Show reviews.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000633 parser.add_option_group(activity_types_group)
634
635 output_format_group = optparse.OptionGroup(parser, 'Output Format',
636 'By default, all activity will be printed in the '
637 'following format: {url} {title}. This can be '
638 'changed for either all activity types or '
639 'individually for each activity type. The format '
640 'is defined as documented for '
641 'string.format(...). The variables available for '
642 'all activity types are url, title and author. '
643 'Format options for specific activity types will '
644 'override the generic format.')
645 output_format_group.add_option(
646 '-f', '--output-format', metavar='<format>',
647 default=u'{url} {title}',
648 help='Specifies the format to use when printing all your activity.')
649 output_format_group.add_option(
650 '--output-format-changes', metavar='<format>',
651 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000652 help='Specifies the format to use when printing changes. Supports the '
653 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000654 output_format_group.add_option(
655 '--output-format-issues', metavar='<format>',
656 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000657 help='Specifies the format to use when printing issues. Supports the '
658 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000659 output_format_group.add_option(
660 '--output-format-reviews', metavar='<format>',
661 default=None,
662 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000663 output_format_group.add_option(
664 '--output-format-heading', metavar='<format>',
665 default=u'{heading}:',
666 help='Specifies the format to use when printing headings.')
667 output_format_group.add_option(
668 '-m', '--markdown', action='store_true',
669 help='Use markdown-friendly output (overrides --output-format '
670 'and --output-format-heading)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000671 parser.add_option_group(output_format_group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000672 auth.add_auth_options(parser)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000673
674 # Remove description formatting
675 parser.format_description = (
676 lambda _: parser.description) # pylint: disable=E1101
677
678 options, args = parser.parse_args()
679 options.local_user = os.environ.get('USER')
680 if args:
681 parser.error('Args unsupported')
682 if not options.user:
683 parser.error('USER is not set, please use -u')
684
685 options.user = username(options.user)
686
687 if not options.begin:
688 if options.last_quarter:
689 begin, end = quarter_begin, quarter_end
690 elif options.this_year:
691 begin, end = get_year_of(datetime.today())
692 elif options.week_of:
693 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000694 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000695 begin, end = (get_week_of(datetime.today() -
696 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000697 else:
698 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
699 else:
700 begin = datetime.strptime(options.begin, '%m/%d/%y')
701 if options.end:
702 end = datetime.strptime(options.end, '%m/%d/%y')
703 else:
704 end = datetime.today()
705 options.begin, options.end = begin, end
706
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000707 if options.markdown:
708 options.output_format = ' * [{title}]({url})'
709 options.output_format_heading = '### {heading} ###'
710
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000711 print 'Searching for activity by %s' % options.user
712 print 'Using range %s to %s' % (options.begin, options.end)
713
714 my_activity = MyActivity(options)
715
716 if not (options.changes or options.reviews or options.issues):
717 options.changes = True
718 options.issues = True
719 options.reviews = True
720
721 # First do any required authentication so none of the user interaction has to
722 # wait for actual work.
723 if options.changes:
724 my_activity.auth_for_changes()
725 if options.reviews:
726 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000727
728 print 'Looking up activity.....'
729
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000730 try:
731 if options.changes:
732 my_activity.get_changes()
733 if options.reviews:
734 my_activity.get_reviews()
735 if options.issues:
736 my_activity.get_issues()
737 except auth.AuthenticationError as e:
738 print "auth.AuthenticationError: %s" % e
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000739
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000740 print '\n\n\n'
741
742 my_activity.print_changes()
743 my_activity.print_reviews()
744 my_activity.print_issues()
745 return 0
746
747
748if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000749 # Fix encoding to support non-ascii issue titles.
750 fix_encoding.fix_encoding()
751
sbc@chromium.org013731e2015-02-26 18:28:43 +0000752 try:
753 sys.exit(main())
754 except KeyboardInterrupt:
755 sys.stderr.write('interrupted\n')
756 sys.exit(1)