blob: e7af09e3d670deafef4756a827f715e0c5585903 [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 },
kjellander@chromium.org363c10c2015-03-16 10:12:22 +000087 {
88 'url': 'webrtc-codereview.appspot.com',
89 'shorturl': 'go/rtcrev',
90 'supports_owner_modified_query': True,
91 'requires_auth': False,
92 'email_domain': 'webrtc.org',
93 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000094]
95
96gerrit_instances = [
97 {
deymo@chromium.org6c039202013-09-12 12:28:12 +000098 'url': 'chromium-review.googlesource.com',
deymo@chromium.orge52bd5a2013-08-29 18:05:21 +000099 'shorturl': 'crosreview.com',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000100 },
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000101 {
102 'url': 'chrome-internal-review.googlesource.com',
103 'shorturl': 'crosreview.com/i',
104 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000105]
106
107google_code_projects = [
108 {
deymo@chromium.org69bf3ad2015-02-02 22:19:46 +0000109 'name': 'brillo',
110 'shorturl': 'brbug.com',
111 },
112 {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000113 'name': 'chromium',
114 'shorturl': 'crbug.com',
115 },
116 {
117 'name': 'chromium-os',
deymo@chromium.orgc840e212013-02-13 20:40:22 +0000118 'shorturl': 'crosbug.com',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000119 },
120 {
121 'name': 'chrome-os-partner',
122 },
123 {
124 'name': 'google-breakpad',
125 },
126 {
127 'name': 'gyp',
enne@chromium.orgf01fad32012-11-26 18:09:38 +0000128 },
129 {
130 'name': 'skia',
131 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000132]
133
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000134def username(email):
135 """Keeps the username of an email address."""
136 return email and email.split('@', 1)[0]
137
138
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000139def datetime_to_midnight(date):
140 return date - timedelta(hours=date.hour, minutes=date.minute,
141 seconds=date.second, microseconds=date.microsecond)
142
143
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000144def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000145 begin = (datetime_to_midnight(date) -
146 relativedelta(months=(date.month % 3) - 1, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000147 return begin, begin + relativedelta(months=3)
148
149
150def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000151 begin = (datetime_to_midnight(date) -
152 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000153 return begin, begin + relativedelta(years=1)
154
155
156def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000157 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000158 return begin, begin + timedelta(days=7)
159
160
161def get_yes_or_no(msg):
162 while True:
163 response = raw_input(msg + ' yes/no [no] ')
164 if response == 'y' or response == 'yes':
165 return True
166 elif not response or response == 'n' or response == 'no':
167 return False
168
169
deymo@chromium.org6c039202013-09-12 12:28:12 +0000170def datetime_from_gerrit(date_string):
171 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
172
173
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000174def datetime_from_rietveld(date_string):
deymo@chromium.org29eb6e62014-03-20 01:55:55 +0000175 try:
176 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f')
177 except ValueError:
178 # Sometimes rietveld returns a value without the milliseconds part, so we
179 # attempt to parse those cases as well.
180 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000181
182
183def datetime_from_google_code(date_string):
184 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S.%fZ')
185
186
187class MyActivity(object):
188 def __init__(self, options):
189 self.options = options
190 self.modified_after = options.begin
191 self.modified_before = options.end
192 self.user = options.user
193 self.changes = []
194 self.reviews = []
195 self.issues = []
196 self.check_cookies()
197 self.google_code_auth_token = None
198
199 # Check the codereview cookie jar to determine which Rietveld instances to
200 # authenticate to.
201 def check_cookies(self):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000202 filtered_instances = []
203
204 def has_cookie(instance):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000205 auth_config = auth.extract_auth_config_from_options(self.options)
206 a = auth.get_authenticator_for_host(instance['url'], auth_config)
207 return a.has_cached_credentials()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000208
209 for instance in rietveld_instances:
210 instance['auth'] = has_cookie(instance)
211
212 if filtered_instances:
213 print ('No cookie found for the following Rietveld instance%s:' %
214 ('s' if len(filtered_instances) > 1 else ''))
215 for instance in filtered_instances:
216 print '\t' + instance['url']
217 print 'Use --auth if you would like to authenticate to them.\n'
218
219 def rietveld_search(self, instance, owner=None, reviewer=None):
220 if instance['requires_auth'] and not instance['auth']:
221 return []
222
223
224 email = None if instance['auth'] else ''
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000225 auth_config = auth.extract_auth_config_from_options(self.options)
226 remote = rietveld.Rietveld('https://' + instance['url'], auth_config, email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000227
228 # See def search() in rietveld.py to see all the filters you can use.
229 query_modified_after = None
230
231 if instance['supports_owner_modified_query']:
232 query_modified_after = self.modified_after.strftime('%Y-%m-%d')
233
234 # Rietveld does not allow search by both created_before and modified_after.
235 # (And some instances don't allow search by both owner and modified_after)
236 owner_email = None
237 reviewer_email = None
238 if owner:
239 owner_email = owner + '@' + instance['email_domain']
240 if reviewer:
241 reviewer_email = reviewer + '@' + instance['email_domain']
242 issues = remote.search(
243 owner=owner_email,
244 reviewer=reviewer_email,
245 modified_after=query_modified_after,
246 with_messages=True)
247
248 issues = filter(
249 lambda i: (datetime_from_rietveld(i['created']) < self.modified_before),
250 issues)
251 issues = filter(
252 lambda i: (datetime_from_rietveld(i['modified']) > self.modified_after),
253 issues)
254
255 should_filter_by_user = True
256 issues = map(partial(self.process_rietveld_issue, instance), issues)
257 issues = filter(
258 partial(self.filter_issue, should_filter_by_user=should_filter_by_user),
259 issues)
260 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
261
262 return issues
263
264 def process_rietveld_issue(self, instance, issue):
265 ret = {}
266 ret['owner'] = issue['owner_email']
267 ret['author'] = ret['owner']
268
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000269 ret['reviewers'] = set(issue['reviewers'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000270
271 shorturl = instance['url']
272 if 'shorturl' in instance:
273 shorturl = instance['shorturl']
274
275 ret['review_url'] = 'http://%s/%d' % (shorturl, issue['issue'])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000276
277 # Rietveld sometimes has '\r\n' instead of '\n'.
278 ret['header'] = issue['description'].replace('\r', '').split('\n')[0]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000279
280 ret['modified'] = datetime_from_rietveld(issue['modified'])
281 ret['created'] = datetime_from_rietveld(issue['created'])
282 ret['replies'] = self.process_rietveld_replies(issue['messages'])
283
284 return ret
285
286 @staticmethod
287 def process_rietveld_replies(replies):
288 ret = []
289 for reply in replies:
290 r = {}
291 r['author'] = reply['sender']
292 r['created'] = datetime_from_rietveld(reply['date'])
293 r['content'] = ''
294 ret.append(r)
295 return ret
296
deymo@chromium.org6c039202013-09-12 12:28:12 +0000297 @staticmethod
298 def gerrit_changes_over_ssh(instance, filters):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000299 # See https://review.openstack.org/Documentation/cmd-query.html
300 # Gerrit doesn't allow filtering by created time, only modified time.
deymo@chromium.org6c039202013-09-12 12:28:12 +0000301 gquery_cmd = ['ssh', '-p', str(instance['port']), instance['host'],
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000302 'gerrit', 'query',
303 '--format', 'JSON',
304 '--comments',
deymo@chromium.org6c039202013-09-12 12:28:12 +0000305 '--'] + filters
306 (stdout, _) = subprocess.Popen(gquery_cmd, stdout=subprocess.PIPE,
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000307 stderr=subprocess.PIPE).communicate()
deymo@chromium.org6c039202013-09-12 12:28:12 +0000308 # Drop the last line of the output with the stats.
309 issues = stdout.splitlines()[:-1]
310 return map(json.loads, issues)
311
312 @staticmethod
313 def gerrit_changes_over_rest(instance, filters):
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000314 # Convert the "key:value" filter to a dictionary.
315 req = dict(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000316 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000317 # Instantiate the generator to force all the requests now and catch the
318 # errors here.
319 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
320 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS']))
321 except gerrit_util.GerritError, e:
322 print 'ERROR: Looking up %r: %s' % (instance['url'], e)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000323 return []
324
deymo@chromium.org6c039202013-09-12 12:28:12 +0000325 def gerrit_search(self, instance, owner=None, reviewer=None):
326 max_age = datetime.today() - self.modified_after
327 max_age = max_age.days * 24 * 3600 + max_age.seconds
328 user_filter = 'owner:%s' % owner if owner else 'reviewer:%s' % reviewer
329 filters = ['-age:%ss' % max_age, user_filter]
330
331 # Determine the gerrit interface to use: SSH or REST API:
332 if 'host' in instance:
333 issues = self.gerrit_changes_over_ssh(instance, filters)
334 issues = [self.process_gerrit_ssh_issue(instance, issue)
335 for issue in issues]
336 elif 'url' in instance:
337 issues = self.gerrit_changes_over_rest(instance, filters)
338 issues = [self.process_gerrit_rest_issue(instance, issue)
339 for issue in issues]
340 else:
341 raise Exception('Invalid gerrit_instances configuration.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000342
343 # TODO(cjhopman): should we filter abandoned changes?
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000344 issues = filter(self.filter_issue, issues)
345 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
346
347 return issues
348
deymo@chromium.org6c039202013-09-12 12:28:12 +0000349 def process_gerrit_ssh_issue(self, instance, issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000350 ret = {}
351 ret['review_url'] = issue['url']
deymo@chromium.orge52bd5a2013-08-29 18:05:21 +0000352 if 'shorturl' in instance:
353 ret['review_url'] = 'http://%s/%s' % (instance['shorturl'],
354 issue['number'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000355 ret['header'] = issue['subject']
356 ret['owner'] = issue['owner']['email']
357 ret['author'] = ret['owner']
358 ret['created'] = datetime.fromtimestamp(issue['createdOn'])
359 ret['modified'] = datetime.fromtimestamp(issue['lastUpdated'])
360 if 'comments' in issue:
deymo@chromium.org6c039202013-09-12 12:28:12 +0000361 ret['replies'] = self.process_gerrit_ssh_issue_replies(issue['comments'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000362 else:
363 ret['replies'] = []
deymo@chromium.org6c039202013-09-12 12:28:12 +0000364 ret['reviewers'] = set(r['author'] for r in ret['replies'])
365 ret['reviewers'].discard(ret['author'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000366 return ret
367
368 @staticmethod
deymo@chromium.org6c039202013-09-12 12:28:12 +0000369 def process_gerrit_ssh_issue_replies(replies):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000370 ret = []
371 replies = filter(lambda r: 'email' in r['reviewer'], replies)
372 for reply in replies:
deymo@chromium.org6c039202013-09-12 12:28:12 +0000373 ret.append({
374 'author': reply['reviewer']['email'],
375 'created': datetime.fromtimestamp(reply['timestamp']),
376 'content': '',
377 })
378 return ret
379
380 def process_gerrit_rest_issue(self, instance, issue):
381 ret = {}
382 ret['review_url'] = 'https://%s/%s' % (instance['url'], issue['_number'])
383 if 'shorturl' in instance:
384 # TODO(deymo): Move this short link to https once crosreview.com supports
385 # it.
386 ret['review_url'] = 'http://%s/%s' % (instance['shorturl'],
387 issue['_number'])
388 ret['header'] = issue['subject']
389 ret['owner'] = issue['owner']['email']
390 ret['author'] = ret['owner']
391 ret['created'] = datetime_from_gerrit(issue['created'])
392 ret['modified'] = datetime_from_gerrit(issue['updated'])
393 if 'messages' in issue:
394 ret['replies'] = self.process_gerrit_rest_issue_replies(issue['messages'])
395 else:
396 ret['replies'] = []
397 ret['reviewers'] = set(r['author'] for r in ret['replies'])
398 ret['reviewers'].discard(ret['author'])
399 return ret
400
401 @staticmethod
402 def process_gerrit_rest_issue_replies(replies):
403 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000404 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
405 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000406 for reply in replies:
407 ret.append({
408 'author': reply['author']['email'],
409 'created': datetime_from_gerrit(reply['date']),
410 'content': reply['message'],
411 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000412 return ret
413
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000414 def project_hosting_issue_search(self, instance):
415 auth_config = auth.extract_auth_config_from_options(self.options)
416 authenticator = auth.get_authenticator_for_host(
417 "code.google.com", auth_config)
418 http = authenticator.authorize(httplib2.Http())
419 url = "https://www.googleapis.com/projecthosting/v2/projects/%s/issues" % (
420 instance["name"])
421 epoch = datetime.utcfromtimestamp(0)
422 user_str = '%s@chromium.org' % self.user
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000423
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000424 query_data = urllib.urlencode({
425 'maxResults': 10000,
426 'q': user_str,
427 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
428 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000429 })
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000430 url = url + '?' + query_data
431 _, body = http.request(url)
432 content = json.loads(body)
433 if not content:
434 print "Unable to parse %s response from projecthosting." % (
435 instance["name"])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000436 return []
437
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000438 issues = []
439 if 'items' in content:
440 items = content['items']
441 for item in items:
442 issue = {
443 "header": item["title"],
444 "created": item["published"],
445 "modified": item["updated"],
446 "author": item["author"]["name"],
447 "url": "https://code.google.com/p/%s/issues/detail?id=%s" % (
448 instance["name"], item["id"]),
449 "comments": []
450 }
451 if 'owner' in item:
452 issue['owner'] = item['owner']['name']
453 else:
454 issue['owner'] = 'None'
455 if issue['owner'] == user_str or issue['author'] == user_str:
456 issues.append(issue)
457
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000458 return issues
459
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000460 def print_heading(self, heading):
461 print
462 print self.options.output_format_heading.format(heading=heading)
463
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000464 def print_change(self, change):
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000465 optional_values = {
466 'reviewers': ', '.join(change['reviewers'])
467 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000468 self.print_generic(self.options.output_format,
469 self.options.output_format_changes,
470 change['header'],
471 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000472 change['author'],
473 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000474
475 def print_issue(self, issue):
476 optional_values = {
477 'owner': issue['owner'],
478 }
479 self.print_generic(self.options.output_format,
480 self.options.output_format_issues,
481 issue['header'],
482 issue['url'],
483 issue['author'],
484 optional_values)
485
486 def print_review(self, review):
487 self.print_generic(self.options.output_format,
488 self.options.output_format_reviews,
489 review['header'],
490 review['review_url'],
491 review['author'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000492
493 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000494 def print_generic(default_fmt, specific_fmt,
495 title, url, author,
496 optional_values=None):
497 output_format = specific_fmt if specific_fmt is not None else default_fmt
498 output_format = unicode(output_format)
499 required_values = {
500 'title': title,
501 'url': url,
502 'author': author,
503 }
504 # Merge required and optional values.
505 if optional_values is not None:
506 values = dict(required_values.items() + optional_values.items())
507 else:
508 values = required_values
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000509 print output_format.format(**values).encode(sys.getdefaultencoding())
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000510
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000511
512 def filter_issue(self, issue, should_filter_by_user=True):
513 def maybe_filter_username(email):
514 return not should_filter_by_user or username(email) == self.user
515 if (maybe_filter_username(issue['author']) and
516 self.filter_modified(issue['created'])):
517 return True
518 if (maybe_filter_username(issue['owner']) and
519 (self.filter_modified(issue['created']) or
520 self.filter_modified(issue['modified']))):
521 return True
522 for reply in issue['replies']:
523 if self.filter_modified(reply['created']):
524 if not should_filter_by_user:
525 break
526 if (username(reply['author']) == self.user
527 or (self.user + '@') in reply['content']):
528 break
529 else:
530 return False
531 return True
532
533 def filter_modified(self, modified):
534 return self.modified_after < modified and modified < self.modified_before
535
536 def auth_for_changes(self):
537 #TODO(cjhopman): Move authentication check for getting changes here.
538 pass
539
540 def auth_for_reviews(self):
541 # Reviews use all the same instances as changes so no authentication is
542 # required.
543 pass
544
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000545 def get_changes(self):
546 for instance in rietveld_instances:
547 self.changes += self.rietveld_search(instance, owner=self.user)
548
549 for instance in gerrit_instances:
550 self.changes += self.gerrit_search(instance, owner=self.user)
551
552 def print_changes(self):
553 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000554 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000555 for change in self.changes:
556 self.print_change(change)
557
558 def get_reviews(self):
559 for instance in rietveld_instances:
560 self.reviews += self.rietveld_search(instance, reviewer=self.user)
561
562 for instance in gerrit_instances:
563 reviews = self.gerrit_search(instance, reviewer=self.user)
564 reviews = filter(lambda r: not username(r['owner']) == self.user, reviews)
565 self.reviews += reviews
566
567 def print_reviews(self):
568 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000569 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000570 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000571 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000572
573 def get_issues(self):
574 for project in google_code_projects:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000575 self.issues += self.project_hosting_issue_search(project)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000576
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000577 def print_issues(self):
578 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000579 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000580 for issue in self.issues:
581 self.print_issue(issue)
582
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000583 def print_activity(self):
584 self.print_changes()
585 self.print_reviews()
586 self.print_issues()
587
588
589def main():
590 # Silence upload.py.
591 rietveld.upload.verbosity = 0
592
593 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
594 parser.add_option(
595 '-u', '--user', metavar='<email>',
596 default=os.environ.get('USER'),
597 help='Filter on user, default=%default')
598 parser.add_option(
599 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000600 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000601 parser.add_option(
602 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000603 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000604 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
605 relativedelta(months=2))
606 parser.add_option(
607 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000608 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000609 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
610 parser.add_option(
611 '-Y', '--this_year', action='store_true',
612 help='Use this year\'s dates')
613 parser.add_option(
614 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000615 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000616 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000617 '-W', '--last_week', action='count',
618 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000619 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000620 '-a', '--auth',
621 action='store_true',
622 help='Ask to authenticate for instances with no auth cookie')
623
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000624 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000625 'By default, all activity will be looked up and '
626 'printed. If any of these are specified, only '
627 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000628 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000629 '-c', '--changes',
630 action='store_true',
631 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000632 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000633 '-i', '--issues',
634 action='store_true',
635 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000636 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000637 '-r', '--reviews',
638 action='store_true',
639 help='Show reviews.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000640 parser.add_option_group(activity_types_group)
641
642 output_format_group = optparse.OptionGroup(parser, 'Output Format',
643 'By default, all activity will be printed in the '
644 'following format: {url} {title}. This can be '
645 'changed for either all activity types or '
646 'individually for each activity type. The format '
647 'is defined as documented for '
648 'string.format(...). The variables available for '
649 'all activity types are url, title and author. '
650 'Format options for specific activity types will '
651 'override the generic format.')
652 output_format_group.add_option(
653 '-f', '--output-format', metavar='<format>',
654 default=u'{url} {title}',
655 help='Specifies the format to use when printing all your activity.')
656 output_format_group.add_option(
657 '--output-format-changes', metavar='<format>',
658 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000659 help='Specifies the format to use when printing changes. Supports the '
660 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000661 output_format_group.add_option(
662 '--output-format-issues', metavar='<format>',
663 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000664 help='Specifies the format to use when printing issues. Supports the '
665 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000666 output_format_group.add_option(
667 '--output-format-reviews', metavar='<format>',
668 default=None,
669 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000670 output_format_group.add_option(
671 '--output-format-heading', metavar='<format>',
672 default=u'{heading}:',
673 help='Specifies the format to use when printing headings.')
674 output_format_group.add_option(
675 '-m', '--markdown', action='store_true',
676 help='Use markdown-friendly output (overrides --output-format '
677 'and --output-format-heading)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000678 parser.add_option_group(output_format_group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000679 auth.add_auth_options(parser)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000680
681 # Remove description formatting
682 parser.format_description = (
683 lambda _: parser.description) # pylint: disable=E1101
684
685 options, args = parser.parse_args()
686 options.local_user = os.environ.get('USER')
687 if args:
688 parser.error('Args unsupported')
689 if not options.user:
690 parser.error('USER is not set, please use -u')
691
692 options.user = username(options.user)
693
694 if not options.begin:
695 if options.last_quarter:
696 begin, end = quarter_begin, quarter_end
697 elif options.this_year:
698 begin, end = get_year_of(datetime.today())
699 elif options.week_of:
700 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000701 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000702 begin, end = (get_week_of(datetime.today() -
703 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000704 else:
705 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
706 else:
707 begin = datetime.strptime(options.begin, '%m/%d/%y')
708 if options.end:
709 end = datetime.strptime(options.end, '%m/%d/%y')
710 else:
711 end = datetime.today()
712 options.begin, options.end = begin, end
713
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000714 if options.markdown:
715 options.output_format = ' * [{title}]({url})'
716 options.output_format_heading = '### {heading} ###'
717
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000718 print 'Searching for activity by %s' % options.user
719 print 'Using range %s to %s' % (options.begin, options.end)
720
721 my_activity = MyActivity(options)
722
723 if not (options.changes or options.reviews or options.issues):
724 options.changes = True
725 options.issues = True
726 options.reviews = True
727
728 # First do any required authentication so none of the user interaction has to
729 # wait for actual work.
730 if options.changes:
731 my_activity.auth_for_changes()
732 if options.reviews:
733 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000734
735 print 'Looking up activity.....'
736
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000737 try:
738 if options.changes:
739 my_activity.get_changes()
740 if options.reviews:
741 my_activity.get_reviews()
742 if options.issues:
743 my_activity.get_issues()
744 except auth.AuthenticationError as e:
745 print "auth.AuthenticationError: %s" % e
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000746
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000747 print '\n\n\n'
748
749 my_activity.print_changes()
750 my_activity.print_reviews()
751 my_activity.print_issues()
752 return 0
753
754
755if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000756 # Fix encoding to support non-ascii issue titles.
757 fix_encoding.fix_encoding()
758
sbc@chromium.org013731e2015-02-26 18:28:43 +0000759 try:
760 sys.exit(main())
761 except KeyboardInterrupt:
762 sys.stderr.write('interrupted\n')
763 sys.exit(1)