blob: d0305e44d526ba5b486f6abeaed14e3dd27eccdc [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:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080049 from dateutil.relativedelta import relativedelta # pylint: disable=import-error
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000050except ImportError:
51 print 'python-dateutil package required'
52 exit(1)
53
54# python-keyring provides easy access to the system keyring.
55try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080056 import keyring # pylint: disable=unused-import,F0401
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000057except 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 }
jdoerrie64287842017-01-09 14:40:26 +0100436 if 'shorturl' in instance:
437 issue['url'] = 'http://%s/%d' % (instance['shorturl'], item['id'])
438
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000439 if 'owner' in item:
440 issue['owner'] = item['owner']['name']
441 else:
442 issue['owner'] = 'None'
443 if issue['owner'] == user_str or issue['author'] == user_str:
444 issues.append(issue)
445
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000446 return issues
447
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000448 def print_heading(self, heading):
449 print
450 print self.options.output_format_heading.format(heading=heading)
451
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000452 def print_change(self, change):
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000453 optional_values = {
454 'reviewers': ', '.join(change['reviewers'])
455 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000456 self.print_generic(self.options.output_format,
457 self.options.output_format_changes,
458 change['header'],
459 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000460 change['author'],
461 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000462
463 def print_issue(self, issue):
464 optional_values = {
465 'owner': issue['owner'],
466 }
467 self.print_generic(self.options.output_format,
468 self.options.output_format_issues,
469 issue['header'],
470 issue['url'],
471 issue['author'],
472 optional_values)
473
474 def print_review(self, review):
475 self.print_generic(self.options.output_format,
476 self.options.output_format_reviews,
477 review['header'],
478 review['review_url'],
479 review['author'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000480
481 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000482 def print_generic(default_fmt, specific_fmt,
483 title, url, author,
484 optional_values=None):
485 output_format = specific_fmt if specific_fmt is not None else default_fmt
486 output_format = unicode(output_format)
487 required_values = {
488 'title': title,
489 'url': url,
490 'author': author,
491 }
492 # Merge required and optional values.
493 if optional_values is not None:
494 values = dict(required_values.items() + optional_values.items())
495 else:
496 values = required_values
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000497 print output_format.format(**values).encode(sys.getdefaultencoding())
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000498
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000499
500 def filter_issue(self, issue, should_filter_by_user=True):
501 def maybe_filter_username(email):
502 return not should_filter_by_user or username(email) == self.user
503 if (maybe_filter_username(issue['author']) and
504 self.filter_modified(issue['created'])):
505 return True
506 if (maybe_filter_username(issue['owner']) and
507 (self.filter_modified(issue['created']) or
508 self.filter_modified(issue['modified']))):
509 return True
510 for reply in issue['replies']:
511 if self.filter_modified(reply['created']):
512 if not should_filter_by_user:
513 break
514 if (username(reply['author']) == self.user
515 or (self.user + '@') in reply['content']):
516 break
517 else:
518 return False
519 return True
520
521 def filter_modified(self, modified):
522 return self.modified_after < modified and modified < self.modified_before
523
524 def auth_for_changes(self):
525 #TODO(cjhopman): Move authentication check for getting changes here.
526 pass
527
528 def auth_for_reviews(self):
529 # Reviews use all the same instances as changes so no authentication is
530 # required.
531 pass
532
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000533 def get_changes(self):
534 for instance in rietveld_instances:
535 self.changes += self.rietveld_search(instance, owner=self.user)
536
537 for instance in gerrit_instances:
538 self.changes += self.gerrit_search(instance, owner=self.user)
539
540 def print_changes(self):
541 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000542 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000543 for change in self.changes:
544 self.print_change(change)
545
546 def get_reviews(self):
547 for instance in rietveld_instances:
548 self.reviews += self.rietveld_search(instance, reviewer=self.user)
549
550 for instance in gerrit_instances:
551 reviews = self.gerrit_search(instance, reviewer=self.user)
552 reviews = filter(lambda r: not username(r['owner']) == self.user, reviews)
553 self.reviews += reviews
554
555 def print_reviews(self):
556 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000557 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000558 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000559 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000560
561 def get_issues(self):
562 for project in google_code_projects:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000563 self.issues += self.project_hosting_issue_search(project)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000564
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000565 def print_issues(self):
566 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000567 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000568 for issue in self.issues:
569 self.print_issue(issue)
570
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000571 def print_activity(self):
572 self.print_changes()
573 self.print_reviews()
574 self.print_issues()
575
576
577def main():
578 # Silence upload.py.
579 rietveld.upload.verbosity = 0
580
581 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
582 parser.add_option(
583 '-u', '--user', metavar='<email>',
584 default=os.environ.get('USER'),
585 help='Filter on user, default=%default')
586 parser.add_option(
587 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000588 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000589 parser.add_option(
590 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000591 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000592 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
593 relativedelta(months=2))
594 parser.add_option(
595 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000596 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000597 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
598 parser.add_option(
599 '-Y', '--this_year', action='store_true',
600 help='Use this year\'s dates')
601 parser.add_option(
602 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000603 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000604 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000605 '-W', '--last_week', action='count',
606 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000607 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000608 '-a', '--auth',
609 action='store_true',
610 help='Ask to authenticate for instances with no auth cookie')
611
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000612 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000613 'By default, all activity will be looked up and '
614 'printed. If any of these are specified, only '
615 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000616 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000617 '-c', '--changes',
618 action='store_true',
619 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000620 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000621 '-i', '--issues',
622 action='store_true',
623 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000624 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000625 '-r', '--reviews',
626 action='store_true',
627 help='Show reviews.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000628 parser.add_option_group(activity_types_group)
629
630 output_format_group = optparse.OptionGroup(parser, 'Output Format',
631 'By default, all activity will be printed in the '
632 'following format: {url} {title}. This can be '
633 'changed for either all activity types or '
634 'individually for each activity type. The format '
635 'is defined as documented for '
636 'string.format(...). The variables available for '
637 'all activity types are url, title and author. '
638 'Format options for specific activity types will '
639 'override the generic format.')
640 output_format_group.add_option(
641 '-f', '--output-format', metavar='<format>',
642 default=u'{url} {title}',
643 help='Specifies the format to use when printing all your activity.')
644 output_format_group.add_option(
645 '--output-format-changes', metavar='<format>',
646 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000647 help='Specifies the format to use when printing changes. Supports the '
648 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000649 output_format_group.add_option(
650 '--output-format-issues', metavar='<format>',
651 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000652 help='Specifies the format to use when printing issues. Supports the '
653 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000654 output_format_group.add_option(
655 '--output-format-reviews', metavar='<format>',
656 default=None,
657 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000658 output_format_group.add_option(
659 '--output-format-heading', metavar='<format>',
660 default=u'{heading}:',
661 help='Specifies the format to use when printing headings.')
662 output_format_group.add_option(
663 '-m', '--markdown', action='store_true',
664 help='Use markdown-friendly output (overrides --output-format '
665 'and --output-format-heading)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000666 parser.add_option_group(output_format_group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000667 auth.add_auth_options(parser)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000668
669 # Remove description formatting
670 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800671 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000672
673 options, args = parser.parse_args()
674 options.local_user = os.environ.get('USER')
675 if args:
676 parser.error('Args unsupported')
677 if not options.user:
678 parser.error('USER is not set, please use -u')
679
680 options.user = username(options.user)
681
682 if not options.begin:
683 if options.last_quarter:
684 begin, end = quarter_begin, quarter_end
685 elif options.this_year:
686 begin, end = get_year_of(datetime.today())
687 elif options.week_of:
688 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000689 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000690 begin, end = (get_week_of(datetime.today() -
691 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000692 else:
693 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
694 else:
695 begin = datetime.strptime(options.begin, '%m/%d/%y')
696 if options.end:
697 end = datetime.strptime(options.end, '%m/%d/%y')
698 else:
699 end = datetime.today()
700 options.begin, options.end = begin, end
701
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000702 if options.markdown:
703 options.output_format = ' * [{title}]({url})'
704 options.output_format_heading = '### {heading} ###'
705
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000706 print 'Searching for activity by %s' % options.user
707 print 'Using range %s to %s' % (options.begin, options.end)
708
709 my_activity = MyActivity(options)
710
711 if not (options.changes or options.reviews or options.issues):
712 options.changes = True
713 options.issues = True
714 options.reviews = True
715
716 # First do any required authentication so none of the user interaction has to
717 # wait for actual work.
718 if options.changes:
719 my_activity.auth_for_changes()
720 if options.reviews:
721 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000722
723 print 'Looking up activity.....'
724
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000725 try:
726 if options.changes:
727 my_activity.get_changes()
728 if options.reviews:
729 my_activity.get_reviews()
730 if options.issues:
731 my_activity.get_issues()
732 except auth.AuthenticationError as e:
733 print "auth.AuthenticationError: %s" % e
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000734
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000735 print '\n\n\n'
736
737 my_activity.print_changes()
738 my_activity.print_reviews()
739 my_activity.print_issues()
740 return 0
741
742
743if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000744 # Fix encoding to support non-ascii issue titles.
745 fix_encoding.fix_encoding()
746
sbc@chromium.org013731e2015-02-26 18:28:43 +0000747 try:
748 sys.exit(main())
749 except KeyboardInterrupt:
750 sys.stderr.write('interrupted\n')
751 sys.exit(1)