blob: 92763d1d719767e69bee33679926f7f704cf4357 [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 },
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000098 {
99 'url': 'android-review.googlesource.com',
100 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000101]
102
103google_code_projects = [
104 {
105 'name': 'chromium',
106 'shorturl': 'crbug.com',
107 },
108 {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000109 'name': 'google-breakpad',
110 },
111 {
112 'name': 'gyp',
enne@chromium.orgf01fad32012-11-26 18:09:38 +0000113 },
114 {
115 'name': 'skia',
116 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000117]
118
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000119def username(email):
120 """Keeps the username of an email address."""
121 return email and email.split('@', 1)[0]
122
123
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000124def datetime_to_midnight(date):
125 return date - timedelta(hours=date.hour, minutes=date.minute,
126 seconds=date.second, microseconds=date.microsecond)
127
128
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000129def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000130 begin = (datetime_to_midnight(date) -
131 relativedelta(months=(date.month % 3) - 1, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000132 return begin, begin + relativedelta(months=3)
133
134
135def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000136 begin = (datetime_to_midnight(date) -
137 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000138 return begin, begin + relativedelta(years=1)
139
140
141def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000142 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000143 return begin, begin + timedelta(days=7)
144
145
146def get_yes_or_no(msg):
147 while True:
148 response = raw_input(msg + ' yes/no [no] ')
149 if response == 'y' or response == 'yes':
150 return True
151 elif not response or response == 'n' or response == 'no':
152 return False
153
154
deymo@chromium.org6c039202013-09-12 12:28:12 +0000155def datetime_from_gerrit(date_string):
156 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
157
158
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000159def datetime_from_rietveld(date_string):
deymo@chromium.org29eb6e62014-03-20 01:55:55 +0000160 try:
161 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f')
162 except ValueError:
163 # Sometimes rietveld returns a value without the milliseconds part, so we
164 # attempt to parse those cases as well.
165 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000166
167
168def datetime_from_google_code(date_string):
169 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S.%fZ')
170
171
172class MyActivity(object):
173 def __init__(self, options):
174 self.options = options
175 self.modified_after = options.begin
176 self.modified_before = options.end
177 self.user = options.user
178 self.changes = []
179 self.reviews = []
180 self.issues = []
181 self.check_cookies()
182 self.google_code_auth_token = None
183
184 # Check the codereview cookie jar to determine which Rietveld instances to
185 # authenticate to.
186 def check_cookies(self):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000187 filtered_instances = []
188
189 def has_cookie(instance):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000190 auth_config = auth.extract_auth_config_from_options(self.options)
191 a = auth.get_authenticator_for_host(instance['url'], auth_config)
192 return a.has_cached_credentials()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000193
194 for instance in rietveld_instances:
195 instance['auth'] = has_cookie(instance)
196
197 if filtered_instances:
198 print ('No cookie found for the following Rietveld instance%s:' %
199 ('s' if len(filtered_instances) > 1 else ''))
200 for instance in filtered_instances:
201 print '\t' + instance['url']
202 print 'Use --auth if you would like to authenticate to them.\n'
203
204 def rietveld_search(self, instance, owner=None, reviewer=None):
205 if instance['requires_auth'] and not instance['auth']:
206 return []
207
208
209 email = None if instance['auth'] else ''
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000210 auth_config = auth.extract_auth_config_from_options(self.options)
211 remote = rietveld.Rietveld('https://' + instance['url'], auth_config, email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000212
213 # See def search() in rietveld.py to see all the filters you can use.
214 query_modified_after = None
215
216 if instance['supports_owner_modified_query']:
217 query_modified_after = self.modified_after.strftime('%Y-%m-%d')
218
219 # Rietveld does not allow search by both created_before and modified_after.
220 # (And some instances don't allow search by both owner and modified_after)
221 owner_email = None
222 reviewer_email = None
223 if owner:
224 owner_email = owner + '@' + instance['email_domain']
225 if reviewer:
226 reviewer_email = reviewer + '@' + instance['email_domain']
227 issues = remote.search(
228 owner=owner_email,
229 reviewer=reviewer_email,
230 modified_after=query_modified_after,
231 with_messages=True)
232
233 issues = filter(
234 lambda i: (datetime_from_rietveld(i['created']) < self.modified_before),
235 issues)
236 issues = filter(
237 lambda i: (datetime_from_rietveld(i['modified']) > self.modified_after),
238 issues)
239
240 should_filter_by_user = True
241 issues = map(partial(self.process_rietveld_issue, instance), issues)
242 issues = filter(
243 partial(self.filter_issue, should_filter_by_user=should_filter_by_user),
244 issues)
245 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
246
247 return issues
248
249 def process_rietveld_issue(self, instance, issue):
250 ret = {}
251 ret['owner'] = issue['owner_email']
252 ret['author'] = ret['owner']
253
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000254 ret['reviewers'] = set(issue['reviewers'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000255
256 shorturl = instance['url']
257 if 'shorturl' in instance:
258 shorturl = instance['shorturl']
259
260 ret['review_url'] = 'http://%s/%d' % (shorturl, issue['issue'])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000261
262 # Rietveld sometimes has '\r\n' instead of '\n'.
263 ret['header'] = issue['description'].replace('\r', '').split('\n')[0]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000264
265 ret['modified'] = datetime_from_rietveld(issue['modified'])
266 ret['created'] = datetime_from_rietveld(issue['created'])
267 ret['replies'] = self.process_rietveld_replies(issue['messages'])
268
269 return ret
270
271 @staticmethod
272 def process_rietveld_replies(replies):
273 ret = []
274 for reply in replies:
275 r = {}
276 r['author'] = reply['sender']
277 r['created'] = datetime_from_rietveld(reply['date'])
278 r['content'] = ''
279 ret.append(r)
280 return ret
281
deymo@chromium.org6c039202013-09-12 12:28:12 +0000282 @staticmethod
283 def gerrit_changes_over_ssh(instance, filters):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000284 # See https://review.openstack.org/Documentation/cmd-query.html
285 # Gerrit doesn't allow filtering by created time, only modified time.
deymo@chromium.org6c039202013-09-12 12:28:12 +0000286 gquery_cmd = ['ssh', '-p', str(instance['port']), instance['host'],
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000287 'gerrit', 'query',
288 '--format', 'JSON',
289 '--comments',
deymo@chromium.org6c039202013-09-12 12:28:12 +0000290 '--'] + filters
291 (stdout, _) = subprocess.Popen(gquery_cmd, stdout=subprocess.PIPE,
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000292 stderr=subprocess.PIPE).communicate()
deymo@chromium.org6c039202013-09-12 12:28:12 +0000293 # Drop the last line of the output with the stats.
294 issues = stdout.splitlines()[:-1]
295 return map(json.loads, issues)
296
297 @staticmethod
298 def gerrit_changes_over_rest(instance, filters):
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000299 # Convert the "key:value" filter to a dictionary.
300 req = dict(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000301 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000302 # Instantiate the generator to force all the requests now and catch the
303 # errors here.
304 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
305 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS']))
306 except gerrit_util.GerritError, e:
307 print 'ERROR: Looking up %r: %s' % (instance['url'], e)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000308 return []
309
deymo@chromium.org6c039202013-09-12 12:28:12 +0000310 def gerrit_search(self, instance, owner=None, reviewer=None):
311 max_age = datetime.today() - self.modified_after
312 max_age = max_age.days * 24 * 3600 + max_age.seconds
313 user_filter = 'owner:%s' % owner if owner else 'reviewer:%s' % reviewer
314 filters = ['-age:%ss' % max_age, user_filter]
315
316 # Determine the gerrit interface to use: SSH or REST API:
317 if 'host' in instance:
318 issues = self.gerrit_changes_over_ssh(instance, filters)
319 issues = [self.process_gerrit_ssh_issue(instance, issue)
320 for issue in issues]
321 elif 'url' in instance:
322 issues = self.gerrit_changes_over_rest(instance, filters)
323 issues = [self.process_gerrit_rest_issue(instance, issue)
324 for issue in issues]
325 else:
326 raise Exception('Invalid gerrit_instances configuration.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000327
328 # TODO(cjhopman): should we filter abandoned changes?
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000329 issues = filter(self.filter_issue, issues)
330 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
331
332 return issues
333
deymo@chromium.org6c039202013-09-12 12:28:12 +0000334 def process_gerrit_ssh_issue(self, instance, issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000335 ret = {}
336 ret['review_url'] = issue['url']
deymo@chromium.orge52bd5a2013-08-29 18:05:21 +0000337 if 'shorturl' in instance:
338 ret['review_url'] = 'http://%s/%s' % (instance['shorturl'],
339 issue['number'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000340 ret['header'] = issue['subject']
341 ret['owner'] = issue['owner']['email']
342 ret['author'] = ret['owner']
343 ret['created'] = datetime.fromtimestamp(issue['createdOn'])
344 ret['modified'] = datetime.fromtimestamp(issue['lastUpdated'])
345 if 'comments' in issue:
deymo@chromium.org6c039202013-09-12 12:28:12 +0000346 ret['replies'] = self.process_gerrit_ssh_issue_replies(issue['comments'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000347 else:
348 ret['replies'] = []
deymo@chromium.org6c039202013-09-12 12:28:12 +0000349 ret['reviewers'] = set(r['author'] for r in ret['replies'])
350 ret['reviewers'].discard(ret['author'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000351 return ret
352
353 @staticmethod
deymo@chromium.org6c039202013-09-12 12:28:12 +0000354 def process_gerrit_ssh_issue_replies(replies):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000355 ret = []
356 replies = filter(lambda r: 'email' in r['reviewer'], replies)
357 for reply in replies:
deymo@chromium.org6c039202013-09-12 12:28:12 +0000358 ret.append({
359 'author': reply['reviewer']['email'],
360 'created': datetime.fromtimestamp(reply['timestamp']),
361 'content': '',
362 })
363 return ret
364
365 def process_gerrit_rest_issue(self, instance, issue):
366 ret = {}
367 ret['review_url'] = 'https://%s/%s' % (instance['url'], issue['_number'])
368 if 'shorturl' in instance:
369 # TODO(deymo): Move this short link to https once crosreview.com supports
370 # it.
371 ret['review_url'] = 'http://%s/%s' % (instance['shorturl'],
372 issue['_number'])
373 ret['header'] = issue['subject']
374 ret['owner'] = issue['owner']['email']
375 ret['author'] = ret['owner']
376 ret['created'] = datetime_from_gerrit(issue['created'])
377 ret['modified'] = datetime_from_gerrit(issue['updated'])
378 if 'messages' in issue:
379 ret['replies'] = self.process_gerrit_rest_issue_replies(issue['messages'])
380 else:
381 ret['replies'] = []
382 ret['reviewers'] = set(r['author'] for r in ret['replies'])
383 ret['reviewers'].discard(ret['author'])
384 return ret
385
386 @staticmethod
387 def process_gerrit_rest_issue_replies(replies):
388 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000389 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
390 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000391 for reply in replies:
392 ret.append({
393 'author': reply['author']['email'],
394 'created': datetime_from_gerrit(reply['date']),
395 'content': reply['message'],
396 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000397 return ret
398
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000399 def project_hosting_issue_search(self, instance):
400 auth_config = auth.extract_auth_config_from_options(self.options)
401 authenticator = auth.get_authenticator_for_host(
sheyang@chromium.org8ea011c2016-03-01 18:36:22 +0000402 "bugs.chromium.org", auth_config)
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000403 http = authenticator.authorize(httplib2.Http())
sheyang@chromium.org8ea011c2016-03-01 18:36:22 +0000404 url = ("https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects"
405 "/%s/issues") % instance["name"]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000406 epoch = datetime.utcfromtimestamp(0)
407 user_str = '%s@chromium.org' % self.user
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000408
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000409 query_data = urllib.urlencode({
410 'maxResults': 10000,
411 'q': user_str,
412 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
413 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000414 })
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000415 url = url + '?' + query_data
416 _, body = http.request(url)
417 content = json.loads(body)
418 if not content:
419 print "Unable to parse %s response from projecthosting." % (
420 instance["name"])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000421 return []
422
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000423 issues = []
424 if 'items' in content:
425 items = content['items']
426 for item in items:
427 issue = {
428 "header": item["title"],
429 "created": item["published"],
430 "modified": item["updated"],
431 "author": item["author"]["name"],
432 "url": "https://code.google.com/p/%s/issues/detail?id=%s" % (
433 instance["name"], item["id"]),
434 "comments": []
435 }
436 if 'owner' in item:
437 issue['owner'] = item['owner']['name']
438 else:
439 issue['owner'] = 'None'
440 if issue['owner'] == user_str or issue['author'] == user_str:
441 issues.append(issue)
442
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000443 return issues
444
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000445 def print_heading(self, heading):
446 print
447 print self.options.output_format_heading.format(heading=heading)
448
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000449 def print_change(self, change):
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000450 optional_values = {
451 'reviewers': ', '.join(change['reviewers'])
452 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000453 self.print_generic(self.options.output_format,
454 self.options.output_format_changes,
455 change['header'],
456 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000457 change['author'],
458 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000459
460 def print_issue(self, issue):
461 optional_values = {
462 'owner': issue['owner'],
463 }
464 self.print_generic(self.options.output_format,
465 self.options.output_format_issues,
466 issue['header'],
467 issue['url'],
468 issue['author'],
469 optional_values)
470
471 def print_review(self, review):
472 self.print_generic(self.options.output_format,
473 self.options.output_format_reviews,
474 review['header'],
475 review['review_url'],
476 review['author'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000477
478 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000479 def print_generic(default_fmt, specific_fmt,
480 title, url, author,
481 optional_values=None):
482 output_format = specific_fmt if specific_fmt is not None else default_fmt
483 output_format = unicode(output_format)
484 required_values = {
485 'title': title,
486 'url': url,
487 'author': author,
488 }
489 # Merge required and optional values.
490 if optional_values is not None:
491 values = dict(required_values.items() + optional_values.items())
492 else:
493 values = required_values
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000494 print output_format.format(**values).encode(sys.getdefaultencoding())
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000495
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000496
497 def filter_issue(self, issue, should_filter_by_user=True):
498 def maybe_filter_username(email):
499 return not should_filter_by_user or username(email) == self.user
500 if (maybe_filter_username(issue['author']) and
501 self.filter_modified(issue['created'])):
502 return True
503 if (maybe_filter_username(issue['owner']) and
504 (self.filter_modified(issue['created']) or
505 self.filter_modified(issue['modified']))):
506 return True
507 for reply in issue['replies']:
508 if self.filter_modified(reply['created']):
509 if not should_filter_by_user:
510 break
511 if (username(reply['author']) == self.user
512 or (self.user + '@') in reply['content']):
513 break
514 else:
515 return False
516 return True
517
518 def filter_modified(self, modified):
519 return self.modified_after < modified and modified < self.modified_before
520
521 def auth_for_changes(self):
522 #TODO(cjhopman): Move authentication check for getting changes here.
523 pass
524
525 def auth_for_reviews(self):
526 # Reviews use all the same instances as changes so no authentication is
527 # required.
528 pass
529
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000530 def get_changes(self):
531 for instance in rietveld_instances:
532 self.changes += self.rietveld_search(instance, owner=self.user)
533
534 for instance in gerrit_instances:
535 self.changes += self.gerrit_search(instance, owner=self.user)
536
537 def print_changes(self):
538 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000539 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000540 for change in self.changes:
541 self.print_change(change)
542
543 def get_reviews(self):
544 for instance in rietveld_instances:
545 self.reviews += self.rietveld_search(instance, reviewer=self.user)
546
547 for instance in gerrit_instances:
548 reviews = self.gerrit_search(instance, reviewer=self.user)
549 reviews = filter(lambda r: not username(r['owner']) == self.user, reviews)
550 self.reviews += reviews
551
552 def print_reviews(self):
553 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000554 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000555 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000556 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000557
558 def get_issues(self):
559 for project in google_code_projects:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000560 self.issues += self.project_hosting_issue_search(project)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000561
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000562 def print_issues(self):
563 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000564 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000565 for issue in self.issues:
566 self.print_issue(issue)
567
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000568 def print_activity(self):
569 self.print_changes()
570 self.print_reviews()
571 self.print_issues()
572
573
574def main():
575 # Silence upload.py.
576 rietveld.upload.verbosity = 0
577
578 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
579 parser.add_option(
580 '-u', '--user', metavar='<email>',
581 default=os.environ.get('USER'),
582 help='Filter on user, default=%default')
583 parser.add_option(
584 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000585 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000586 parser.add_option(
587 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000588 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000589 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
590 relativedelta(months=2))
591 parser.add_option(
592 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000593 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000594 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
595 parser.add_option(
596 '-Y', '--this_year', action='store_true',
597 help='Use this year\'s dates')
598 parser.add_option(
599 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000600 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000601 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000602 '-W', '--last_week', action='count',
603 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000604 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000605 '-a', '--auth',
606 action='store_true',
607 help='Ask to authenticate for instances with no auth cookie')
608
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000609 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000610 'By default, all activity will be looked up and '
611 'printed. If any of these are specified, only '
612 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000613 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000614 '-c', '--changes',
615 action='store_true',
616 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000617 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000618 '-i', '--issues',
619 action='store_true',
620 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000621 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000622 '-r', '--reviews',
623 action='store_true',
624 help='Show reviews.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000625 parser.add_option_group(activity_types_group)
626
627 output_format_group = optparse.OptionGroup(parser, 'Output Format',
628 'By default, all activity will be printed in the '
629 'following format: {url} {title}. This can be '
630 'changed for either all activity types or '
631 'individually for each activity type. The format '
632 'is defined as documented for '
633 'string.format(...). The variables available for '
634 'all activity types are url, title and author. '
635 'Format options for specific activity types will '
636 'override the generic format.')
637 output_format_group.add_option(
638 '-f', '--output-format', metavar='<format>',
639 default=u'{url} {title}',
640 help='Specifies the format to use when printing all your activity.')
641 output_format_group.add_option(
642 '--output-format-changes', metavar='<format>',
643 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000644 help='Specifies the format to use when printing changes. Supports the '
645 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000646 output_format_group.add_option(
647 '--output-format-issues', metavar='<format>',
648 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000649 help='Specifies the format to use when printing issues. Supports the '
650 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000651 output_format_group.add_option(
652 '--output-format-reviews', metavar='<format>',
653 default=None,
654 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000655 output_format_group.add_option(
656 '--output-format-heading', metavar='<format>',
657 default=u'{heading}:',
658 help='Specifies the format to use when printing headings.')
659 output_format_group.add_option(
660 '-m', '--markdown', action='store_true',
661 help='Use markdown-friendly output (overrides --output-format '
662 'and --output-format-heading)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000663 parser.add_option_group(output_format_group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000664 auth.add_auth_options(parser)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000665
666 # Remove description formatting
667 parser.format_description = (
668 lambda _: parser.description) # pylint: disable=E1101
669
670 options, args = parser.parse_args()
671 options.local_user = os.environ.get('USER')
672 if args:
673 parser.error('Args unsupported')
674 if not options.user:
675 parser.error('USER is not set, please use -u')
676
677 options.user = username(options.user)
678
679 if not options.begin:
680 if options.last_quarter:
681 begin, end = quarter_begin, quarter_end
682 elif options.this_year:
683 begin, end = get_year_of(datetime.today())
684 elif options.week_of:
685 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000686 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000687 begin, end = (get_week_of(datetime.today() -
688 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000689 else:
690 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
691 else:
692 begin = datetime.strptime(options.begin, '%m/%d/%y')
693 if options.end:
694 end = datetime.strptime(options.end, '%m/%d/%y')
695 else:
696 end = datetime.today()
697 options.begin, options.end = begin, end
698
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000699 if options.markdown:
700 options.output_format = ' * [{title}]({url})'
701 options.output_format_heading = '### {heading} ###'
702
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000703 print 'Searching for activity by %s' % options.user
704 print 'Using range %s to %s' % (options.begin, options.end)
705
706 my_activity = MyActivity(options)
707
708 if not (options.changes or options.reviews or options.issues):
709 options.changes = True
710 options.issues = True
711 options.reviews = True
712
713 # First do any required authentication so none of the user interaction has to
714 # wait for actual work.
715 if options.changes:
716 my_activity.auth_for_changes()
717 if options.reviews:
718 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000719
720 print 'Looking up activity.....'
721
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000722 try:
723 if options.changes:
724 my_activity.get_changes()
725 if options.reviews:
726 my_activity.get_reviews()
727 if options.issues:
728 my_activity.get_issues()
729 except auth.AuthenticationError as e:
730 print "auth.AuthenticationError: %s" % e
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000731
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000732 print '\n\n\n'
733
734 my_activity.print_changes()
735 my_activity.print_reviews()
736 my_activity.print_issues()
737 return 0
738
739
740if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000741 # Fix encoding to support non-ascii issue titles.
742 fix_encoding.fix_encoding()
743
sbc@chromium.org013731e2015-02-26 18:28:43 +0000744 try:
745 sys.exit(main())
746 except KeyboardInterrupt:
747 sys.stderr.write('interrupted\n')
748 sys.exit(1)