blob: 0f2079fa886da35f81f3c8627303181b196c4d5d [file] [log] [blame]
Gabriel Charettebc6617a2019-02-05 21:30:52 +00001#!/usr/bin/env vpython
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00002# 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.
Mohamed Heikaldc37feb2019-06-28 22:33:58 +000012 - my_activity.py -b 4/24/19 for stats since April 24th 2019.
13 - my_activity.py -b 4/24/19 -e 6/16/19 stats between April 24th and June 16th.
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +000014 - my_activity.py -jd to output stats for the week to json with deltas data.
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000015"""
16
17# These services typically only provide a created time and a last modified time
18# for each item for general queries. This is not enough to determine if there
19# was activity in a given time period. So, we first query for all things created
20# before end and modified after begin. Then, we get the details of each item and
21# check those details to determine if there was activity in the given period.
22# This means that query time scales mostly with (today() - begin).
23
Gabriel Charettebc6617a2019-02-05 21:30:52 +000024# [VPYTHON:BEGIN]
25# wheel: <
26# name: "infra/python/wheels/python-dateutil-py2_py3"
27# version: "version:2.7.3"
28# >
29# wheel: <
30# name: "infra/python/wheels/six-py2_py3"
31# version: "version:1.10.0"
32# >
33# [VPYTHON:END]
34
Raul Tambre80ee78e2019-05-06 22:41:05 +000035from __future__ import print_function
36
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010037import collections
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +010038import contextlib
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000039from datetime import datetime
40from datetime import timedelta
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010041import itertools
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000042import json
Tobias Sargeantffb3c432017-03-08 14:09:14 +000043import logging
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +010044from multiprocessing.pool import ThreadPool
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000045import optparse
46import os
47import subprocess
Tobias Sargeantffb3c432017-03-08 14:09:14 +000048from string import Formatter
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000049import sys
50import urllib
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +000051import re
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000052
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000053import auth
stevefung@chromium.org832d51e2015-05-27 12:52:51 +000054import fix_encoding
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000055import gerrit_util
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000056
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +000057from third_party import httplib2
58
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000059try:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000060 import dateutil # pylint: disable=import-error
61 import dateutil.parser
62 from dateutil.relativedelta import relativedelta
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000063except ImportError:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000064 logging.error('python-dateutil package required')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000065 exit(1)
66
Tobias Sargeantffb3c432017-03-08 14:09:14 +000067
68class DefaultFormatter(Formatter):
69 def __init__(self, default = ''):
70 super(DefaultFormatter, self).__init__()
71 self.default = default
72
73 def get_value(self, key, args, kwds):
74 if isinstance(key, basestring) and key not in kwds:
75 return self.default
76 return Formatter.get_value(self, key, args, kwds)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000077
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000078gerrit_instances = [
79 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000080 'url': 'android-review.googlesource.com',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000081 },
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000082 {
83 'url': 'chrome-internal-review.googlesource.com',
Jeremy Romanf475a472017-06-06 16:49:11 -040084 'shorturl': 'crrev.com/i',
Varun Khanejad9f97bc2017-08-02 12:55:01 -070085 'short_url_protocol': 'https',
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000086 },
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000087 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000088 'url': 'chromium-review.googlesource.com',
89 'shorturl': 'crrev.com/c',
90 'short_url_protocol': 'https',
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000091 },
Ryan Harrison897602a2017-09-18 16:23:41 -040092 {
Ryan Harrison06e18692019-09-23 18:22:25 +000093 'url': 'dawn-review.googlesource.com',
94 },
95 {
Ryan Harrison897602a2017-09-18 16:23:41 -040096 'url': 'pdfium-review.googlesource.com',
97 },
Adrienne Walker95d4c852018-09-27 20:28:12 +000098 {
99 'url': 'skia-review.googlesource.com',
100 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000101]
102
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100103monorail_projects = {
Jamie Madill1db68ea2019-09-03 19:34:42 +0000104 'angleproject': {
105 'shorturl': 'anglebug.com',
106 'short_url_protocol': 'http',
107 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100108 'chromium': {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000109 'shorturl': 'crbug.com',
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700110 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000111 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000112 'dawn': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100113 'google-breakpad': {},
114 'gyp': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100115 'pdfium': {
Ryan Harrison897602a2017-09-18 16:23:41 -0400116 'shorturl': 'crbug.com/pdfium',
117 'short_url_protocol': 'https',
118 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000119 'skia': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100120 'v8': {
121 'shorturl': 'crbug.com/v8',
122 'short_url_protocol': 'https',
123 },
124}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000125
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000126def username(email):
127 """Keeps the username of an email address."""
128 return email and email.split('@', 1)[0]
129
130
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000131def datetime_to_midnight(date):
132 return date - timedelta(hours=date.hour, minutes=date.minute,
133 seconds=date.second, microseconds=date.microsecond)
134
135
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000136def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000137 begin = (datetime_to_midnight(date) -
138 relativedelta(months=(date.month % 3) - 1, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000139 return begin, begin + relativedelta(months=3)
140
141
142def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000143 begin = (datetime_to_midnight(date) -
144 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000145 return begin, begin + relativedelta(years=1)
146
147
148def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000149 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000150 return begin, begin + timedelta(days=7)
151
152
153def get_yes_or_no(msg):
154 while True:
155 response = raw_input(msg + ' yes/no [no] ')
156 if response == 'y' or response == 'yes':
157 return True
158 elif not response or response == 'n' or response == 'no':
159 return False
160
161
deymo@chromium.org6c039202013-09-12 12:28:12 +0000162def datetime_from_gerrit(date_string):
163 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
164
165
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100166def datetime_from_monorail(date_string):
167 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000168
169
170class MyActivity(object):
171 def __init__(self, options):
172 self.options = options
173 self.modified_after = options.begin
174 self.modified_before = options.end
175 self.user = options.user
176 self.changes = []
177 self.reviews = []
178 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100179 self.referenced_issues = []
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000180 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-14 19:02:55 -0700181 self.access_errors = set()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000182
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100183 def show_progress(self, how='.'):
184 if sys.stdout.isatty():
185 sys.stdout.write(how)
186 sys.stdout.flush()
187
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000188 def extract_bug_numbers_from_description(self, issue):
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000189 description = None
190
191 if 'description' in issue:
192 # Getting the description for Rietveld
193 description = issue['description']
194 elif 'revisions' in issue:
195 # Getting the description for REST Gerrit
196 revision = issue['revisions'][issue['current_revision']]
197 description = revision['commit']['message']
198
199 bugs = []
200 if description:
Nicolas Dossou-gbete903ea732017-07-10 16:46:59 +0100201 # Handle both "Bug: 99999" and "BUG=99999" bug notations
202 # Multiple bugs can be noted on a single line or in multiple ones.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100203 matches = re.findall(
204 r'BUG[=:]\s?((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)', description,
205 flags=re.IGNORECASE)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000206 if matches:
207 for match in matches:
208 bugs.extend(match[0].replace(' ', '').split(','))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100209 # Add default chromium: prefix if none specified.
210 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000211
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000212 return sorted(set(bugs))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000213
Vadim Bendebury8de38002018-05-14 19:02:55 -0700214 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200215 # Convert the "key:value" filter to a list of (key, value) pairs.
216 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000217 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000218 # Instantiate the generator to force all the requests now and catch the
219 # errors here.
220 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000221 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
222 'CURRENT_REVISION', 'CURRENT_COMMIT']))
Raul Tambre7c938462019-05-24 16:35:35 +0000223 except gerrit_util.GerritError as e:
Vadim Bendebury8de38002018-05-14 19:02:55 -0700224 error_message = 'Looking up %r: %s' % (instance['url'], e)
225 if error_message not in self.access_errors:
226 self.access_errors.add(error_message)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000227 return []
228
deymo@chromium.org6c039202013-09-12 12:28:12 +0000229 def gerrit_search(self, instance, owner=None, reviewer=None):
230 max_age = datetime.today() - self.modified_after
Andrii Shyshkalov4aedec02018-09-17 16:31:17 +0000231 filters = ['-age:%ss' % (max_age.days * 24 * 3600 + max_age.seconds)]
232 if owner:
233 assert not reviewer
234 filters.append('owner:%s' % owner)
235 else:
236 filters.extend(('-owner:%s' % reviewer, 'reviewer:%s' % reviewer))
Peter K. Lee9342ac02018-08-28 21:03:51 +0000237 # TODO(cjhopman): Should abandoned changes be filtered out when
238 # merged_only is not enabled?
239 if self.options.merged_only:
240 filters.append('status:merged')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000241
Aaron Gable2979a872017-09-05 17:38:32 -0700242 issues = self.gerrit_changes_over_rest(instance, filters)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100243 self.show_progress()
Aaron Gable2979a872017-09-05 17:38:32 -0700244 issues = [self.process_gerrit_issue(instance, issue)
245 for issue in issues]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000246
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000247 issues = filter(self.filter_issue, issues)
248 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
249
250 return issues
251
Aaron Gable2979a872017-09-05 17:38:32 -0700252 def process_gerrit_issue(self, instance, issue):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000253 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000254 if self.options.deltas:
255 ret['delta'] = DefaultFormatter().format(
256 '+{insertions},-{deletions}',
257 **issue)
258 ret['status'] = issue['status']
deymo@chromium.org6c039202013-09-12 12:28:12 +0000259 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700260 protocol = instance.get('short_url_protocol', 'http')
261 url = instance['shorturl']
262 else:
263 protocol = 'https'
264 url = instance['url']
265 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
266
deymo@chromium.org6c039202013-09-12 12:28:12 +0000267 ret['header'] = issue['subject']
Don Garrett2ebf9fd2018-08-06 15:14:00 +0000268 ret['owner'] = issue['owner'].get('email', '')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000269 ret['author'] = ret['owner']
270 ret['created'] = datetime_from_gerrit(issue['created'])
271 ret['modified'] = datetime_from_gerrit(issue['updated'])
272 if 'messages' in issue:
Aaron Gable2979a872017-09-05 17:38:32 -0700273 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
deymo@chromium.org6c039202013-09-12 12:28:12 +0000274 else:
275 ret['replies'] = []
276 ret['reviewers'] = set(r['author'] for r in ret['replies'])
277 ret['reviewers'].discard(ret['author'])
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000278 ret['bugs'] = self.extract_bug_numbers_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000279 return ret
280
281 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700282 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000283 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000284 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
285 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000286 for reply in replies:
287 ret.append({
288 'author': reply['author']['email'],
289 'created': datetime_from_gerrit(reply['date']),
290 'content': reply['message'],
291 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000292 return ret
293
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100294 def monorail_get_auth_http(self):
Kenneth Russell66badbd2018-09-09 21:35:32 +0000295 # Manually use a long timeout (10m); for some users who have a
296 # long history on the issue tracker, whatever the default timeout
297 # is is reached.
Edward Lemur5b929a42019-10-21 17:57:39 +0000298 return auth.Authenticator().authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100299
300 def filter_modified_monorail_issue(self, issue):
301 """Precisely checks if an issue has been modified in the time range.
302
303 This fetches all issue comments to check if the issue has been modified in
304 the time range specified by user. This is needed because monorail only
305 allows filtering by last updated and published dates, which is not
306 sufficient to tell whether a given issue has been modified at some specific
307 time range. Any update to the issue is a reported as comment on Monorail.
308
309 Args:
310 issue: Issue dict as returned by monorail_query_issues method. In
311 particular, must have a key 'uid' formatted as 'project:issue_id'.
312
313 Returns:
314 Passed issue if modified, None otherwise.
315 """
316 http = self.monorail_get_auth_http()
317 project, issue_id = issue['uid'].split(':')
318 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
319 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
320 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100321 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100322 content = json.loads(body)
323 if not content:
324 logging.error('Unable to parse %s response from monorail.', project)
325 return issue
326
327 for item in content.get('items', []):
328 comment_published = datetime_from_monorail(item['published'])
329 if self.filter_modified(comment_published):
330 return issue
331
332 return None
333
334 def monorail_query_issues(self, project, query):
335 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000336 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100337 '/%s/issues') % project
338 query_data = urllib.urlencode(query)
339 url = url + '?' + query_data
340 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100341 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100342 content = json.loads(body)
343 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100344 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100345 return []
346
347 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100348 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100349 for item in content.get('items', []):
350 if project_config.get('shorturl'):
351 protocol = project_config.get('short_url_protocol', 'http')
352 item_url = '%s://%s/%d' % (
353 protocol, project_config['shorturl'], item['id'])
354 else:
355 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
356 project, item['id'])
357 issue = {
358 'uid': '%s:%s' % (project, item['id']),
359 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100360 'created': datetime_from_monorail(item['published']),
361 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100362 'author': item['author']['name'],
363 'url': item_url,
364 'comments': [],
365 'status': item['status'],
366 'labels': [],
367 'components': []
368 }
369 if 'owner' in item:
370 issue['owner'] = item['owner']['name']
371 else:
372 issue['owner'] = 'None'
373 if 'labels' in item:
374 issue['labels'] = item['labels']
375 if 'components' in item:
376 issue['components'] = item['components']
377 issues.append(issue)
378
379 return issues
380
381 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000382 epoch = datetime.utcfromtimestamp(0)
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000383 # Defaults to @chromium.org email if one wasn't provided on -u option.
384 user_str = (self.options.email if self.options.email.find('@') >= 0
385 else '%s@chromium.org' % self.user)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000386
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100387 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000388 'maxResults': 10000,
389 'q': user_str,
390 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
391 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000392 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000393
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000394 if self.options.completed_issues:
395 return [
396 issue for issue in issues
397 if (self.match(issue['owner']) and
398 issue['status'].lower() in ('verified', 'fixed'))
399 ]
400
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100401 return [
402 issue for issue in issues
403 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000404
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100405 def monorail_get_issues(self, project, issue_ids):
406 return self.monorail_query_issues(project, {
407 'maxResults': 10000,
408 'q': 'id:%s' % ','.join(issue_ids)
409 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000410
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000411 def print_heading(self, heading):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000412 print()
413 print(self.options.output_format_heading.format(heading=heading))
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000414
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000415 def match(self, author):
416 if '@' in self.user:
417 return author == self.user
418 return author.startswith(self.user + '@')
419
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000420 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000421 activity = len([
422 reply
423 for reply in change['replies']
424 if self.match(reply['author'])
425 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000426 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000427 'created': change['created'].date().isoformat(),
428 'modified': change['modified'].date().isoformat(),
429 'reviewers': ', '.join(change['reviewers']),
430 'status': change['status'],
431 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000432 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000433 if self.options.deltas:
434 optional_values['delta'] = change['delta']
435
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000436 self.print_generic(self.options.output_format,
437 self.options.output_format_changes,
438 change['header'],
439 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000440 change['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000441 change['created'],
442 change['modified'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000443 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000444
445 def print_issue(self, issue):
446 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000447 'created': issue['created'].date().isoformat(),
448 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000449 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000450 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000451 }
452 self.print_generic(self.options.output_format,
453 self.options.output_format_issues,
454 issue['header'],
455 issue['url'],
456 issue['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000457 issue['created'],
458 issue['modified'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000459 optional_values)
460
461 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000462 activity = len([
463 reply
464 for reply in review['replies']
465 if self.match(reply['author'])
466 ])
467 optional_values = {
468 'created': review['created'].date().isoformat(),
469 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800470 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000471 'activity': activity,
472 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800473 if self.options.deltas:
474 optional_values['delta'] = review['delta']
475
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000476 self.print_generic(self.options.output_format,
477 self.options.output_format_reviews,
478 review['header'],
479 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000480 review['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000481 review['created'],
482 review['modified'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000483 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000484
485 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000486 def print_generic(default_fmt, specific_fmt,
Lutz Justen860378e2019-03-12 01:10:02 +0000487 title, url, author, created, modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000488 optional_values=None):
489 output_format = specific_fmt if specific_fmt is not None else default_fmt
490 output_format = unicode(output_format)
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000491 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000492 'title': title,
493 'url': url,
494 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000495 'created': created,
496 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000497 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000498 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000499 values.update(optional_values)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000500 print(DefaultFormatter().format(output_format,
501 **values).encode(sys.getdefaultencoding()))
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000502
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000503
504 def filter_issue(self, issue, should_filter_by_user=True):
505 def maybe_filter_username(email):
506 return not should_filter_by_user or username(email) == self.user
507 if (maybe_filter_username(issue['author']) and
508 self.filter_modified(issue['created'])):
509 return True
510 if (maybe_filter_username(issue['owner']) and
511 (self.filter_modified(issue['created']) or
512 self.filter_modified(issue['modified']))):
513 return True
514 for reply in issue['replies']:
515 if self.filter_modified(reply['created']):
516 if not should_filter_by_user:
517 break
518 if (username(reply['author']) == self.user
519 or (self.user + '@') in reply['content']):
520 break
521 else:
522 return False
523 return True
524
525 def filter_modified(self, modified):
526 return self.modified_after < modified and modified < self.modified_before
527
528 def auth_for_changes(self):
529 #TODO(cjhopman): Move authentication check for getting changes here.
530 pass
531
532 def auth_for_reviews(self):
533 # Reviews use all the same instances as changes so no authentication is
534 # required.
535 pass
536
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000537 def get_changes(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000538 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100539 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100540 gerrit_changes = pool.map_async(
541 lambda instance: self.gerrit_search(instance, owner=self.user),
542 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100543 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000544 self.changes = list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000545
546 def print_changes(self):
547 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000548 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000549 for change in self.changes:
Bruce Dawson2afcf222019-02-25 22:07:08 +0000550 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000551
Vadim Bendebury8de38002018-05-14 19:02:55 -0700552 def print_access_errors(self):
553 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400554 logging.error('Access Errors:')
555 for error in self.access_errors:
556 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700557
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000558 def get_reviews(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000559 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100560 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100561 gerrit_reviews = pool.map_async(
562 lambda instance: self.gerrit_search(instance, reviewer=self.user),
563 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100564 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000565 self.reviews = list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000566
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):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100574 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
575 monorail_issues = pool.map(
576 self.monorail_issue_search, monorail_projects.keys())
577 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
578
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700579 if not monorail_issues:
580 return
581
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100582 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
583 filtered_issues = pool.map(
584 self.filter_modified_monorail_issue, monorail_issues)
585 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100586
587 def get_referenced_issues(self):
588 if not self.issues:
589 self.get_issues()
590
591 if not self.changes:
592 self.get_changes()
593
594 referenced_issue_uids = set(itertools.chain.from_iterable(
595 change['bugs'] for change in self.changes))
596 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
597 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
598
599 missing_issues_by_project = collections.defaultdict(list)
600 for issue_uid in missing_issue_uids:
601 project, issue_id = issue_uid.split(':')
602 missing_issues_by_project[project].append(issue_id)
603
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000604 for project, issue_ids in missing_issues_by_project.items():
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100605 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000606
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000607 def print_issues(self):
608 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000609 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000610 for issue in self.issues:
611 self.print_issue(issue)
612
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100613 def print_changes_by_issue(self, skip_empty_own):
614 if not self.issues or not self.changes:
615 return
616
617 self.print_heading('Changes by referenced issue(s)')
618 issues = {issue['uid']: issue for issue in self.issues}
619 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
620 changes_by_issue_uid = collections.defaultdict(list)
621 changes_by_ref_issue_uid = collections.defaultdict(list)
622 changes_without_issue = []
623 for change in self.changes:
624 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000625 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100626 if issue_uid in issues:
627 changes_by_issue_uid[issue_uid].append(change)
628 added = True
629 if issue_uid in ref_issues:
630 changes_by_ref_issue_uid[issue_uid].append(change)
631 added = True
632 if not added:
633 changes_without_issue.append(change)
634
635 # Changes referencing own issues.
636 for issue_uid in issues:
637 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
638 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000639 if changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000640 print()
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000641 for change in changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000642 print(' ', end='') # this prints no newline
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000643 self.print_change(change)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000644 print()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100645
646 # Changes referencing others' issues.
647 for issue_uid in ref_issues:
648 assert changes_by_ref_issue_uid[issue_uid]
649 self.print_issue(ref_issues[issue_uid])
650 for change in changes_by_ref_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000651 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100652 self.print_change(change)
653
654 # Changes referencing no issues.
655 if changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000656 print(self.options.output_format_no_url.format(title='Other changes'))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100657 for change in changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000658 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100659 self.print_change(change)
660
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000661 def print_activity(self):
662 self.print_changes()
663 self.print_reviews()
664 self.print_issues()
665
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000666 def dump_json(self, ignore_keys=None):
667 if ignore_keys is None:
668 ignore_keys = ['replies']
669
670 def format_for_json_dump(in_array):
671 output = {}
672 for item in in_array:
673 url = item.get('url') or item.get('review_url')
674 if not url:
675 raise Exception('Dumped item %s does not specify url' % item)
676 output[url] = dict(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000677 (k, v) for k,v in item.items() if k not in ignore_keys)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000678 return output
679
680 class PythonObjectEncoder(json.JSONEncoder):
681 def default(self, obj): # pylint: disable=method-hidden
682 if isinstance(obj, datetime):
683 return obj.isoformat()
684 if isinstance(obj, set):
685 return list(obj)
686 return json.JSONEncoder.default(self, obj)
687
688 output = {
689 'reviews': format_for_json_dump(self.reviews),
690 'changes': format_for_json_dump(self.changes),
691 'issues': format_for_json_dump(self.issues)
692 }
Raul Tambre80ee78e2019-05-06 22:41:05 +0000693 print(json.dumps(output, indent=2, cls=PythonObjectEncoder))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000694
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000695
696def main():
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000697 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
698 parser.add_option(
699 '-u', '--user', metavar='<email>',
Bruce Dawson84370962019-02-21 05:33:37 +0000700 # Look for USER and USERNAME (Windows) environment variables.
701 default=os.environ.get('USER', os.environ.get('USERNAME')),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000702 help='Filter on user, default=%default')
703 parser.add_option(
704 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000705 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000706 parser.add_option(
707 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000708 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000709 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
710 relativedelta(months=2))
711 parser.add_option(
712 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000713 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000714 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
715 parser.add_option(
716 '-Y', '--this_year', action='store_true',
717 help='Use this year\'s dates')
718 parser.add_option(
719 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000720 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000721 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000722 '-W', '--last_week', action='count',
723 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000724 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000725 '-a', '--auth',
726 action='store_true',
727 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000728 parser.add_option(
729 '-d', '--deltas',
730 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800731 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100732 parser.add_option(
733 '--no-referenced-issues',
734 action='store_true',
735 help='Do not fetch issues referenced by owned changes. Useful in '
736 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100737 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100738 parser.add_option(
739 '--skip-own-issues-without-changes',
740 action='store_true',
741 help='Skips listing own issues without changes when showing changes '
742 'grouped by referenced issue(s). See --changes-by-issue for more '
743 'details.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000744
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000745 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000746 'By default, all activity will be looked up and '
747 'printed. If any of these are specified, only '
748 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000749 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000750 '-c', '--changes',
751 action='store_true',
752 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000753 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000754 '-i', '--issues',
755 action='store_true',
756 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000757 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000758 '-r', '--reviews',
759 action='store_true',
760 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100761 activity_types_group.add_option(
762 '--changes-by-issue', action='store_true',
763 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000764 parser.add_option_group(activity_types_group)
765
766 output_format_group = optparse.OptionGroup(parser, 'Output Format',
767 'By default, all activity will be printed in the '
768 'following format: {url} {title}. This can be '
769 'changed for either all activity types or '
770 'individually for each activity type. The format '
771 'is defined as documented for '
772 'string.format(...). The variables available for '
Lutz Justen860378e2019-03-12 01:10:02 +0000773 'all activity types are url, title, author, '
774 'created and modified. Format options for '
775 'specific activity types will override the '
776 'generic format.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000777 output_format_group.add_option(
778 '-f', '--output-format', metavar='<format>',
779 default=u'{url} {title}',
780 help='Specifies the format to use when printing all your activity.')
781 output_format_group.add_option(
782 '--output-format-changes', metavar='<format>',
783 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000784 help='Specifies the format to use when printing changes. Supports the '
785 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000786 output_format_group.add_option(
787 '--output-format-issues', metavar='<format>',
788 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000789 help='Specifies the format to use when printing issues. Supports the '
790 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000791 output_format_group.add_option(
792 '--output-format-reviews', metavar='<format>',
793 default=None,
794 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000795 output_format_group.add_option(
796 '--output-format-heading', metavar='<format>',
797 default=u'{heading}:',
798 help='Specifies the format to use when printing headings.')
799 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100800 '--output-format-no-url', default='{title}',
801 help='Specifies the format to use when printing activity without url.')
802 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000803 '-m', '--markdown', action='store_true',
804 help='Use markdown-friendly output (overrides --output-format '
805 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000806 output_format_group.add_option(
807 '-j', '--json', action='store_true',
808 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000809 parser.add_option_group(output_format_group)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000810
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000811 parser.add_option(
812 '-v', '--verbose',
813 action='store_const',
814 dest='verbosity',
815 default=logging.WARN,
816 const=logging.INFO,
817 help='Output extra informational messages.'
818 )
819 parser.add_option(
820 '-q', '--quiet',
821 action='store_const',
822 dest='verbosity',
823 const=logging.ERROR,
824 help='Suppress non-error messages.'
825 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000826 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000827 '-M', '--merged-only',
828 action='store_true',
829 dest='merged_only',
830 default=False,
831 help='Shows only changes that have been merged.')
832 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000833 '-C', '--completed-issues',
834 action='store_true',
835 dest='completed_issues',
836 default=False,
837 help='Shows only monorail issues that have completed (Fixed|Verified) '
838 'by the user.')
839 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000840 '-o', '--output', metavar='<file>',
841 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000842
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000843 # Remove description formatting
844 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800845 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000846
847 options, args = parser.parse_args()
848 options.local_user = os.environ.get('USER')
849 if args:
850 parser.error('Args unsupported')
851 if not options.user:
Bruce Dawson84370962019-02-21 05:33:37 +0000852 parser.error('USER/USERNAME is not set, please use -u')
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000853 # Retains the original -u option as the email address.
854 options.email = options.user
855 options.user = username(options.email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000856
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000857 logging.basicConfig(level=options.verbosity)
858
859 # python-keyring provides easy access to the system keyring.
860 try:
861 import keyring # pylint: disable=unused-import,unused-variable,F0401
862 except ImportError:
863 logging.warning('Consider installing python-keyring')
864
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000865 if not options.begin:
866 if options.last_quarter:
867 begin, end = quarter_begin, quarter_end
868 elif options.this_year:
869 begin, end = get_year_of(datetime.today())
870 elif options.week_of:
871 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000872 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000873 begin, end = (get_week_of(datetime.today() -
874 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000875 else:
876 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
877 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700878 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000879 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700880 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000881 else:
882 end = datetime.today()
883 options.begin, options.end = begin, end
Bruce Dawson2afcf222019-02-25 22:07:08 +0000884 if begin >= end:
885 # The queries fail in peculiar ways when the begin date is in the future.
886 # Give a descriptive error message instead.
887 logging.error('Start date (%s) is the same or later than end date (%s)' %
888 (begin, end))
889 return 1
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000890
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000891 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000892 options.output_format_heading = '### {heading}\n'
893 options.output_format = ' * [{title}]({url})'
894 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000895 logging.info('Searching for activity by %s', options.user)
896 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000897
898 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100899 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000900
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100901 if not (options.changes or options.reviews or options.issues or
902 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000903 options.changes = True
904 options.issues = True
905 options.reviews = True
906
907 # First do any required authentication so none of the user interaction has to
908 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100909 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000910 my_activity.auth_for_changes()
911 if options.reviews:
912 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000913
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000914 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000915
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000916 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100917 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000918 my_activity.get_changes()
919 if options.reviews:
920 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100921 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000922 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100923 if not options.no_referenced_issues:
924 my_activity.get_referenced_issues()
Edward Lemur5b929a42019-10-21 17:57:39 +0000925 except auth.LoginRequiredError as e:
926 logging.error('auth.LoginRequiredError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000927
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100928 my_activity.show_progress('\n')
929
Vadim Bendebury8de38002018-05-14 19:02:55 -0700930 my_activity.print_access_errors()
931
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000932 output_file = None
933 try:
934 if options.output:
935 output_file = open(options.output, 'w')
936 logging.info('Printing output to "%s"', options.output)
937 sys.stdout = output_file
938 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700939 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000940 else:
941 if options.json:
942 my_activity.dump_json()
943 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100944 if options.changes:
945 my_activity.print_changes()
946 if options.reviews:
947 my_activity.print_reviews()
948 if options.issues:
949 my_activity.print_issues()
950 if options.changes_by_issue:
951 my_activity.print_changes_by_issue(
952 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000953 finally:
954 if output_file:
955 logging.info('Done printing to file.')
956 sys.stdout = sys.__stdout__
957 output_file.close()
958
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000959 return 0
960
961
962if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000963 # Fix encoding to support non-ascii issue titles.
964 fix_encoding.fix_encoding()
965
sbc@chromium.org013731e2015-02-26 18:28:43 +0000966 try:
967 sys.exit(main())
968 except KeyboardInterrupt:
969 sys.stderr.write('interrupted\n')
970 sys.exit(1)