blob: cecc2fbb19b4a9676014b22bcf11dc5220f45c50 [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 {
deymo@chromium.org69bf3ad2015-02-02 22:19:46 +0000105 'name': 'brillo',
106 'shorturl': 'brbug.com',
107 },
108 {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000109 'name': 'chromium',
110 'shorturl': 'crbug.com',
111 },
112 {
113 'name': 'chromium-os',
deymo@chromium.orgc840e212013-02-13 20:40:22 +0000114 'shorturl': 'crosbug.com',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000115 },
116 {
117 'name': 'chrome-os-partner',
118 },
119 {
120 'name': 'google-breakpad',
121 },
122 {
123 'name': 'gyp',
enne@chromium.orgf01fad32012-11-26 18:09:38 +0000124 },
125 {
126 'name': 'skia',
127 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000128]
129
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000130def username(email):
131 """Keeps the username of an email address."""
132 return email and email.split('@', 1)[0]
133
134
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000135def datetime_to_midnight(date):
136 return date - timedelta(hours=date.hour, minutes=date.minute,
137 seconds=date.second, microseconds=date.microsecond)
138
139
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000140def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000141 begin = (datetime_to_midnight(date) -
142 relativedelta(months=(date.month % 3) - 1, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000143 return begin, begin + relativedelta(months=3)
144
145
146def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000147 begin = (datetime_to_midnight(date) -
148 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000149 return begin, begin + relativedelta(years=1)
150
151
152def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000153 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000154 return begin, begin + timedelta(days=7)
155
156
157def get_yes_or_no(msg):
158 while True:
159 response = raw_input(msg + ' yes/no [no] ')
160 if response == 'y' or response == 'yes':
161 return True
162 elif not response or response == 'n' or response == 'no':
163 return False
164
165
deymo@chromium.org6c039202013-09-12 12:28:12 +0000166def datetime_from_gerrit(date_string):
167 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
168
169
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000170def datetime_from_rietveld(date_string):
deymo@chromium.org29eb6e62014-03-20 01:55:55 +0000171 try:
172 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f')
173 except ValueError:
174 # Sometimes rietveld returns a value without the milliseconds part, so we
175 # attempt to parse those cases as well.
176 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000177
178
179def datetime_from_google_code(date_string):
180 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S.%fZ')
181
182
183class MyActivity(object):
184 def __init__(self, options):
185 self.options = options
186 self.modified_after = options.begin
187 self.modified_before = options.end
188 self.user = options.user
189 self.changes = []
190 self.reviews = []
191 self.issues = []
192 self.check_cookies()
193 self.google_code_auth_token = None
194
195 # Check the codereview cookie jar to determine which Rietveld instances to
196 # authenticate to.
197 def check_cookies(self):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000198 filtered_instances = []
199
200 def has_cookie(instance):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000201 auth_config = auth.extract_auth_config_from_options(self.options)
202 a = auth.get_authenticator_for_host(instance['url'], auth_config)
203 return a.has_cached_credentials()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000204
205 for instance in rietveld_instances:
206 instance['auth'] = has_cookie(instance)
207
208 if filtered_instances:
209 print ('No cookie found for the following Rietveld instance%s:' %
210 ('s' if len(filtered_instances) > 1 else ''))
211 for instance in filtered_instances:
212 print '\t' + instance['url']
213 print 'Use --auth if you would like to authenticate to them.\n'
214
215 def rietveld_search(self, instance, owner=None, reviewer=None):
216 if instance['requires_auth'] and not instance['auth']:
217 return []
218
219
220 email = None if instance['auth'] else ''
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000221 auth_config = auth.extract_auth_config_from_options(self.options)
222 remote = rietveld.Rietveld('https://' + instance['url'], auth_config, email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000223
224 # See def search() in rietveld.py to see all the filters you can use.
225 query_modified_after = None
226
227 if instance['supports_owner_modified_query']:
228 query_modified_after = self.modified_after.strftime('%Y-%m-%d')
229
230 # Rietveld does not allow search by both created_before and modified_after.
231 # (And some instances don't allow search by both owner and modified_after)
232 owner_email = None
233 reviewer_email = None
234 if owner:
235 owner_email = owner + '@' + instance['email_domain']
236 if reviewer:
237 reviewer_email = reviewer + '@' + instance['email_domain']
238 issues = remote.search(
239 owner=owner_email,
240 reviewer=reviewer_email,
241 modified_after=query_modified_after,
242 with_messages=True)
243
244 issues = filter(
245 lambda i: (datetime_from_rietveld(i['created']) < self.modified_before),
246 issues)
247 issues = filter(
248 lambda i: (datetime_from_rietveld(i['modified']) > self.modified_after),
249 issues)
250
251 should_filter_by_user = True
252 issues = map(partial(self.process_rietveld_issue, instance), issues)
253 issues = filter(
254 partial(self.filter_issue, should_filter_by_user=should_filter_by_user),
255 issues)
256 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
257
258 return issues
259
260 def process_rietveld_issue(self, instance, issue):
261 ret = {}
262 ret['owner'] = issue['owner_email']
263 ret['author'] = ret['owner']
264
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000265 ret['reviewers'] = set(issue['reviewers'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000266
267 shorturl = instance['url']
268 if 'shorturl' in instance:
269 shorturl = instance['shorturl']
270
271 ret['review_url'] = 'http://%s/%d' % (shorturl, issue['issue'])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000272
273 # Rietveld sometimes has '\r\n' instead of '\n'.
274 ret['header'] = issue['description'].replace('\r', '').split('\n')[0]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000275
276 ret['modified'] = datetime_from_rietveld(issue['modified'])
277 ret['created'] = datetime_from_rietveld(issue['created'])
278 ret['replies'] = self.process_rietveld_replies(issue['messages'])
279
280 return ret
281
282 @staticmethod
283 def process_rietveld_replies(replies):
284 ret = []
285 for reply in replies:
286 r = {}
287 r['author'] = reply['sender']
288 r['created'] = datetime_from_rietveld(reply['date'])
289 r['content'] = ''
290 ret.append(r)
291 return ret
292
deymo@chromium.org6c039202013-09-12 12:28:12 +0000293 @staticmethod
294 def gerrit_changes_over_ssh(instance, filters):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000295 # See https://review.openstack.org/Documentation/cmd-query.html
296 # Gerrit doesn't allow filtering by created time, only modified time.
deymo@chromium.org6c039202013-09-12 12:28:12 +0000297 gquery_cmd = ['ssh', '-p', str(instance['port']), instance['host'],
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000298 'gerrit', 'query',
299 '--format', 'JSON',
300 '--comments',
deymo@chromium.org6c039202013-09-12 12:28:12 +0000301 '--'] + filters
302 (stdout, _) = subprocess.Popen(gquery_cmd, stdout=subprocess.PIPE,
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000303 stderr=subprocess.PIPE).communicate()
deymo@chromium.org6c039202013-09-12 12:28:12 +0000304 # Drop the last line of the output with the stats.
305 issues = stdout.splitlines()[:-1]
306 return map(json.loads, issues)
307
308 @staticmethod
309 def gerrit_changes_over_rest(instance, filters):
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000310 # Convert the "key:value" filter to a dictionary.
311 req = dict(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000312 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000313 # Instantiate the generator to force all the requests now and catch the
314 # errors here.
315 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
316 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS']))
317 except gerrit_util.GerritError, e:
318 print 'ERROR: Looking up %r: %s' % (instance['url'], e)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000319 return []
320
deymo@chromium.org6c039202013-09-12 12:28:12 +0000321 def gerrit_search(self, instance, owner=None, reviewer=None):
322 max_age = datetime.today() - self.modified_after
323 max_age = max_age.days * 24 * 3600 + max_age.seconds
324 user_filter = 'owner:%s' % owner if owner else 'reviewer:%s' % reviewer
325 filters = ['-age:%ss' % max_age, user_filter]
326
327 # Determine the gerrit interface to use: SSH or REST API:
328 if 'host' in instance:
329 issues = self.gerrit_changes_over_ssh(instance, filters)
330 issues = [self.process_gerrit_ssh_issue(instance, issue)
331 for issue in issues]
332 elif 'url' in instance:
333 issues = self.gerrit_changes_over_rest(instance, filters)
334 issues = [self.process_gerrit_rest_issue(instance, issue)
335 for issue in issues]
336 else:
337 raise Exception('Invalid gerrit_instances configuration.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000338
339 # TODO(cjhopman): should we filter abandoned changes?
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000340 issues = filter(self.filter_issue, issues)
341 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
342
343 return issues
344
deymo@chromium.org6c039202013-09-12 12:28:12 +0000345 def process_gerrit_ssh_issue(self, instance, issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000346 ret = {}
347 ret['review_url'] = issue['url']
deymo@chromium.orge52bd5a2013-08-29 18:05:21 +0000348 if 'shorturl' in instance:
349 ret['review_url'] = 'http://%s/%s' % (instance['shorturl'],
350 issue['number'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000351 ret['header'] = issue['subject']
352 ret['owner'] = issue['owner']['email']
353 ret['author'] = ret['owner']
354 ret['created'] = datetime.fromtimestamp(issue['createdOn'])
355 ret['modified'] = datetime.fromtimestamp(issue['lastUpdated'])
356 if 'comments' in issue:
deymo@chromium.org6c039202013-09-12 12:28:12 +0000357 ret['replies'] = self.process_gerrit_ssh_issue_replies(issue['comments'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000358 else:
359 ret['replies'] = []
deymo@chromium.org6c039202013-09-12 12:28:12 +0000360 ret['reviewers'] = set(r['author'] for r in ret['replies'])
361 ret['reviewers'].discard(ret['author'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000362 return ret
363
364 @staticmethod
deymo@chromium.org6c039202013-09-12 12:28:12 +0000365 def process_gerrit_ssh_issue_replies(replies):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000366 ret = []
367 replies = filter(lambda r: 'email' in r['reviewer'], replies)
368 for reply in replies:
deymo@chromium.org6c039202013-09-12 12:28:12 +0000369 ret.append({
370 'author': reply['reviewer']['email'],
371 'created': datetime.fromtimestamp(reply['timestamp']),
372 'content': '',
373 })
374 return ret
375
376 def process_gerrit_rest_issue(self, instance, issue):
377 ret = {}
378 ret['review_url'] = 'https://%s/%s' % (instance['url'], issue['_number'])
379 if 'shorturl' in instance:
380 # TODO(deymo): Move this short link to https once crosreview.com supports
381 # it.
382 ret['review_url'] = 'http://%s/%s' % (instance['shorturl'],
383 issue['_number'])
384 ret['header'] = issue['subject']
385 ret['owner'] = issue['owner']['email']
386 ret['author'] = ret['owner']
387 ret['created'] = datetime_from_gerrit(issue['created'])
388 ret['modified'] = datetime_from_gerrit(issue['updated'])
389 if 'messages' in issue:
390 ret['replies'] = self.process_gerrit_rest_issue_replies(issue['messages'])
391 else:
392 ret['replies'] = []
393 ret['reviewers'] = set(r['author'] for r in ret['replies'])
394 ret['reviewers'].discard(ret['author'])
395 return ret
396
397 @staticmethod
398 def process_gerrit_rest_issue_replies(replies):
399 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000400 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
401 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000402 for reply in replies:
403 ret.append({
404 'author': reply['author']['email'],
405 'created': datetime_from_gerrit(reply['date']),
406 'content': reply['message'],
407 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000408 return ret
409
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000410 def project_hosting_issue_search(self, instance):
411 auth_config = auth.extract_auth_config_from_options(self.options)
412 authenticator = auth.get_authenticator_for_host(
413 "code.google.com", auth_config)
414 http = authenticator.authorize(httplib2.Http())
415 url = "https://www.googleapis.com/projecthosting/v2/projects/%s/issues" % (
416 instance["name"])
417 epoch = datetime.utcfromtimestamp(0)
418 user_str = '%s@chromium.org' % self.user
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000419
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000420 query_data = urllib.urlencode({
421 'maxResults': 10000,
422 'q': user_str,
423 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
424 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000425 })
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000426 url = url + '?' + query_data
427 _, body = http.request(url)
428 content = json.loads(body)
429 if not content:
430 print "Unable to parse %s response from projecthosting." % (
431 instance["name"])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000432 return []
433
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000434 issues = []
435 if 'items' in content:
436 items = content['items']
437 for item in items:
438 issue = {
439 "header": item["title"],
440 "created": item["published"],
441 "modified": item["updated"],
442 "author": item["author"]["name"],
443 "url": "https://code.google.com/p/%s/issues/detail?id=%s" % (
444 instance["name"], item["id"]),
445 "comments": []
446 }
447 if 'owner' in item:
448 issue['owner'] = item['owner']['name']
449 else:
450 issue['owner'] = 'None'
451 if issue['owner'] == user_str or issue['author'] == user_str:
452 issues.append(issue)
453
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000454 return issues
455
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000456 def print_heading(self, heading):
457 print
458 print self.options.output_format_heading.format(heading=heading)
459
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000460 def print_change(self, change):
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000461 optional_values = {
462 'reviewers': ', '.join(change['reviewers'])
463 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000464 self.print_generic(self.options.output_format,
465 self.options.output_format_changes,
466 change['header'],
467 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000468 change['author'],
469 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000470
471 def print_issue(self, issue):
472 optional_values = {
473 'owner': issue['owner'],
474 }
475 self.print_generic(self.options.output_format,
476 self.options.output_format_issues,
477 issue['header'],
478 issue['url'],
479 issue['author'],
480 optional_values)
481
482 def print_review(self, review):
483 self.print_generic(self.options.output_format,
484 self.options.output_format_reviews,
485 review['header'],
486 review['review_url'],
487 review['author'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000488
489 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000490 def print_generic(default_fmt, specific_fmt,
491 title, url, author,
492 optional_values=None):
493 output_format = specific_fmt if specific_fmt is not None else default_fmt
494 output_format = unicode(output_format)
495 required_values = {
496 'title': title,
497 'url': url,
498 'author': author,
499 }
500 # Merge required and optional values.
501 if optional_values is not None:
502 values = dict(required_values.items() + optional_values.items())
503 else:
504 values = required_values
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000505 print output_format.format(**values).encode(sys.getdefaultencoding())
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000506
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000507
508 def filter_issue(self, issue, should_filter_by_user=True):
509 def maybe_filter_username(email):
510 return not should_filter_by_user or username(email) == self.user
511 if (maybe_filter_username(issue['author']) and
512 self.filter_modified(issue['created'])):
513 return True
514 if (maybe_filter_username(issue['owner']) and
515 (self.filter_modified(issue['created']) or
516 self.filter_modified(issue['modified']))):
517 return True
518 for reply in issue['replies']:
519 if self.filter_modified(reply['created']):
520 if not should_filter_by_user:
521 break
522 if (username(reply['author']) == self.user
523 or (self.user + '@') in reply['content']):
524 break
525 else:
526 return False
527 return True
528
529 def filter_modified(self, modified):
530 return self.modified_after < modified and modified < self.modified_before
531
532 def auth_for_changes(self):
533 #TODO(cjhopman): Move authentication check for getting changes here.
534 pass
535
536 def auth_for_reviews(self):
537 # Reviews use all the same instances as changes so no authentication is
538 # required.
539 pass
540
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000541 def get_changes(self):
542 for instance in rietveld_instances:
543 self.changes += self.rietveld_search(instance, owner=self.user)
544
545 for instance in gerrit_instances:
546 self.changes += self.gerrit_search(instance, owner=self.user)
547
548 def print_changes(self):
549 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000550 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000551 for change in self.changes:
552 self.print_change(change)
553
554 def get_reviews(self):
555 for instance in rietveld_instances:
556 self.reviews += self.rietveld_search(instance, reviewer=self.user)
557
558 for instance in gerrit_instances:
559 reviews = self.gerrit_search(instance, reviewer=self.user)
560 reviews = filter(lambda r: not username(r['owner']) == self.user, reviews)
561 self.reviews += reviews
562
563 def print_reviews(self):
564 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000565 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000566 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000567 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000568
569 def get_issues(self):
570 for project in google_code_projects:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000571 self.issues += self.project_hosting_issue_search(project)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000572
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000573 def print_issues(self):
574 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000575 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000576 for issue in self.issues:
577 self.print_issue(issue)
578
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000579 def print_activity(self):
580 self.print_changes()
581 self.print_reviews()
582 self.print_issues()
583
584
585def main():
586 # Silence upload.py.
587 rietveld.upload.verbosity = 0
588
589 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
590 parser.add_option(
591 '-u', '--user', metavar='<email>',
592 default=os.environ.get('USER'),
593 help='Filter on user, default=%default')
594 parser.add_option(
595 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000596 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000597 parser.add_option(
598 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000599 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000600 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
601 relativedelta(months=2))
602 parser.add_option(
603 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000604 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000605 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
606 parser.add_option(
607 '-Y', '--this_year', action='store_true',
608 help='Use this year\'s dates')
609 parser.add_option(
610 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000611 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000612 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000613 '-W', '--last_week', action='count',
614 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000615 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000616 '-a', '--auth',
617 action='store_true',
618 help='Ask to authenticate for instances with no auth cookie')
619
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000620 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000621 'By default, all activity will be looked up and '
622 'printed. If any of these are specified, only '
623 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000624 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000625 '-c', '--changes',
626 action='store_true',
627 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000628 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000629 '-i', '--issues',
630 action='store_true',
631 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000632 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000633 '-r', '--reviews',
634 action='store_true',
635 help='Show reviews.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000636 parser.add_option_group(activity_types_group)
637
638 output_format_group = optparse.OptionGroup(parser, 'Output Format',
639 'By default, all activity will be printed in the '
640 'following format: {url} {title}. This can be '
641 'changed for either all activity types or '
642 'individually for each activity type. The format '
643 'is defined as documented for '
644 'string.format(...). The variables available for '
645 'all activity types are url, title and author. '
646 'Format options for specific activity types will '
647 'override the generic format.')
648 output_format_group.add_option(
649 '-f', '--output-format', metavar='<format>',
650 default=u'{url} {title}',
651 help='Specifies the format to use when printing all your activity.')
652 output_format_group.add_option(
653 '--output-format-changes', metavar='<format>',
654 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000655 help='Specifies the format to use when printing changes. Supports the '
656 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000657 output_format_group.add_option(
658 '--output-format-issues', metavar='<format>',
659 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000660 help='Specifies the format to use when printing issues. Supports the '
661 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000662 output_format_group.add_option(
663 '--output-format-reviews', metavar='<format>',
664 default=None,
665 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000666 output_format_group.add_option(
667 '--output-format-heading', metavar='<format>',
668 default=u'{heading}:',
669 help='Specifies the format to use when printing headings.')
670 output_format_group.add_option(
671 '-m', '--markdown', action='store_true',
672 help='Use markdown-friendly output (overrides --output-format '
673 'and --output-format-heading)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000674 parser.add_option_group(output_format_group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000675 auth.add_auth_options(parser)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000676
677 # Remove description formatting
678 parser.format_description = (
679 lambda _: parser.description) # pylint: disable=E1101
680
681 options, args = parser.parse_args()
682 options.local_user = os.environ.get('USER')
683 if args:
684 parser.error('Args unsupported')
685 if not options.user:
686 parser.error('USER is not set, please use -u')
687
688 options.user = username(options.user)
689
690 if not options.begin:
691 if options.last_quarter:
692 begin, end = quarter_begin, quarter_end
693 elif options.this_year:
694 begin, end = get_year_of(datetime.today())
695 elif options.week_of:
696 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000697 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000698 begin, end = (get_week_of(datetime.today() -
699 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000700 else:
701 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
702 else:
703 begin = datetime.strptime(options.begin, '%m/%d/%y')
704 if options.end:
705 end = datetime.strptime(options.end, '%m/%d/%y')
706 else:
707 end = datetime.today()
708 options.begin, options.end = begin, end
709
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000710 if options.markdown:
711 options.output_format = ' * [{title}]({url})'
712 options.output_format_heading = '### {heading} ###'
713
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000714 print 'Searching for activity by %s' % options.user
715 print 'Using range %s to %s' % (options.begin, options.end)
716
717 my_activity = MyActivity(options)
718
719 if not (options.changes or options.reviews or options.issues):
720 options.changes = True
721 options.issues = True
722 options.reviews = True
723
724 # First do any required authentication so none of the user interaction has to
725 # wait for actual work.
726 if options.changes:
727 my_activity.auth_for_changes()
728 if options.reviews:
729 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000730
731 print 'Looking up activity.....'
732
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000733 try:
734 if options.changes:
735 my_activity.get_changes()
736 if options.reviews:
737 my_activity.get_reviews()
738 if options.issues:
739 my_activity.get_issues()
740 except auth.AuthenticationError as e:
741 print "auth.AuthenticationError: %s" % e
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000742
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000743 print '\n\n\n'
744
745 my_activity.print_changes()
746 my_activity.print_reviews()
747 my_activity.print_issues()
748 return 0
749
750
751if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000752 # Fix encoding to support non-ascii issue titles.
753 fix_encoding.fix_encoding()
754
sbc@chromium.org013731e2015-02-26 18:28:43 +0000755 try:
756 sys.exit(main())
757 except KeyboardInterrupt:
758 sys.stderr.write('interrupted\n')
759 sys.exit(1)