blob: 5620ca10d76c4db2ca44b506dd0103c67dd0c023 [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):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000295 auth_config = auth.extract_auth_config_from_options(self.options)
Edward Lemurb4a587d2019-10-09 23:56:38 +0000296 authenticator = auth.get_authenticator(auth_config)
Kenneth Russell66badbd2018-09-09 21:35:32 +0000297 # Manually use a long timeout (10m); for some users who have a
298 # long history on the issue tracker, whatever the default timeout
299 # is is reached.
300 return authenticator.authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100301
302 def filter_modified_monorail_issue(self, issue):
303 """Precisely checks if an issue has been modified in the time range.
304
305 This fetches all issue comments to check if the issue has been modified in
306 the time range specified by user. This is needed because monorail only
307 allows filtering by last updated and published dates, which is not
308 sufficient to tell whether a given issue has been modified at some specific
309 time range. Any update to the issue is a reported as comment on Monorail.
310
311 Args:
312 issue: Issue dict as returned by monorail_query_issues method. In
313 particular, must have a key 'uid' formatted as 'project:issue_id'.
314
315 Returns:
316 Passed issue if modified, None otherwise.
317 """
318 http = self.monorail_get_auth_http()
319 project, issue_id = issue['uid'].split(':')
320 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
321 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
322 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100323 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100324 content = json.loads(body)
325 if not content:
326 logging.error('Unable to parse %s response from monorail.', project)
327 return issue
328
329 for item in content.get('items', []):
330 comment_published = datetime_from_monorail(item['published'])
331 if self.filter_modified(comment_published):
332 return issue
333
334 return None
335
336 def monorail_query_issues(self, project, query):
337 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000338 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100339 '/%s/issues') % project
340 query_data = urllib.urlencode(query)
341 url = url + '?' + query_data
342 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100343 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100344 content = json.loads(body)
345 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100346 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100347 return []
348
349 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100350 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100351 for item in content.get('items', []):
352 if project_config.get('shorturl'):
353 protocol = project_config.get('short_url_protocol', 'http')
354 item_url = '%s://%s/%d' % (
355 protocol, project_config['shorturl'], item['id'])
356 else:
357 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
358 project, item['id'])
359 issue = {
360 'uid': '%s:%s' % (project, item['id']),
361 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100362 'created': datetime_from_monorail(item['published']),
363 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100364 'author': item['author']['name'],
365 'url': item_url,
366 'comments': [],
367 'status': item['status'],
368 'labels': [],
369 'components': []
370 }
371 if 'owner' in item:
372 issue['owner'] = item['owner']['name']
373 else:
374 issue['owner'] = 'None'
375 if 'labels' in item:
376 issue['labels'] = item['labels']
377 if 'components' in item:
378 issue['components'] = item['components']
379 issues.append(issue)
380
381 return issues
382
383 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000384 epoch = datetime.utcfromtimestamp(0)
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000385 # Defaults to @chromium.org email if one wasn't provided on -u option.
386 user_str = (self.options.email if self.options.email.find('@') >= 0
387 else '%s@chromium.org' % self.user)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000388
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100389 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000390 'maxResults': 10000,
391 'q': user_str,
392 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
393 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000394 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000395
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000396 if self.options.completed_issues:
397 return [
398 issue for issue in issues
399 if (self.match(issue['owner']) and
400 issue['status'].lower() in ('verified', 'fixed'))
401 ]
402
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100403 return [
404 issue for issue in issues
405 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000406
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100407 def monorail_get_issues(self, project, issue_ids):
408 return self.monorail_query_issues(project, {
409 'maxResults': 10000,
410 'q': 'id:%s' % ','.join(issue_ids)
411 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000412
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000413 def print_heading(self, heading):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000414 print()
415 print(self.options.output_format_heading.format(heading=heading))
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000416
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000417 def match(self, author):
418 if '@' in self.user:
419 return author == self.user
420 return author.startswith(self.user + '@')
421
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000422 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000423 activity = len([
424 reply
425 for reply in change['replies']
426 if self.match(reply['author'])
427 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000428 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000429 'created': change['created'].date().isoformat(),
430 'modified': change['modified'].date().isoformat(),
431 'reviewers': ', '.join(change['reviewers']),
432 'status': change['status'],
433 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000434 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000435 if self.options.deltas:
436 optional_values['delta'] = change['delta']
437
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000438 self.print_generic(self.options.output_format,
439 self.options.output_format_changes,
440 change['header'],
441 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000442 change['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000443 change['created'],
444 change['modified'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000445 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000446
447 def print_issue(self, issue):
448 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000449 'created': issue['created'].date().isoformat(),
450 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000451 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000452 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000453 }
454 self.print_generic(self.options.output_format,
455 self.options.output_format_issues,
456 issue['header'],
457 issue['url'],
458 issue['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000459 issue['created'],
460 issue['modified'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000461 optional_values)
462
463 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000464 activity = len([
465 reply
466 for reply in review['replies']
467 if self.match(reply['author'])
468 ])
469 optional_values = {
470 'created': review['created'].date().isoformat(),
471 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800472 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000473 'activity': activity,
474 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800475 if self.options.deltas:
476 optional_values['delta'] = review['delta']
477
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000478 self.print_generic(self.options.output_format,
479 self.options.output_format_reviews,
480 review['header'],
481 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000482 review['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000483 review['created'],
484 review['modified'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000485 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000486
487 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000488 def print_generic(default_fmt, specific_fmt,
Lutz Justen860378e2019-03-12 01:10:02 +0000489 title, url, author, created, modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000490 optional_values=None):
491 output_format = specific_fmt if specific_fmt is not None else default_fmt
492 output_format = unicode(output_format)
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000493 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000494 'title': title,
495 'url': url,
496 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000497 'created': created,
498 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000499 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000500 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000501 values.update(optional_values)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000502 print(DefaultFormatter().format(output_format,
503 **values).encode(sys.getdefaultencoding()))
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000504
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000505
506 def filter_issue(self, issue, should_filter_by_user=True):
507 def maybe_filter_username(email):
508 return not should_filter_by_user or username(email) == self.user
509 if (maybe_filter_username(issue['author']) and
510 self.filter_modified(issue['created'])):
511 return True
512 if (maybe_filter_username(issue['owner']) and
513 (self.filter_modified(issue['created']) or
514 self.filter_modified(issue['modified']))):
515 return True
516 for reply in issue['replies']:
517 if self.filter_modified(reply['created']):
518 if not should_filter_by_user:
519 break
520 if (username(reply['author']) == self.user
521 or (self.user + '@') in reply['content']):
522 break
523 else:
524 return False
525 return True
526
527 def filter_modified(self, modified):
528 return self.modified_after < modified and modified < self.modified_before
529
530 def auth_for_changes(self):
531 #TODO(cjhopman): Move authentication check for getting changes here.
532 pass
533
534 def auth_for_reviews(self):
535 # Reviews use all the same instances as changes so no authentication is
536 # required.
537 pass
538
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000539 def get_changes(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000540 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100541 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100542 gerrit_changes = pool.map_async(
543 lambda instance: self.gerrit_search(instance, owner=self.user),
544 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100545 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000546 self.changes = list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000547
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:
Bruce Dawson2afcf222019-02-25 22:07:08 +0000552 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000553
Vadim Bendebury8de38002018-05-14 19:02:55 -0700554 def print_access_errors(self):
555 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400556 logging.error('Access Errors:')
557 for error in self.access_errors:
558 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700559
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000560 def get_reviews(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000561 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100562 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100563 gerrit_reviews = pool.map_async(
564 lambda instance: self.gerrit_search(instance, reviewer=self.user),
565 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100566 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000567 self.reviews = list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000568
569 def print_reviews(self):
570 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000571 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000572 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000573 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000574
575 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100576 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
577 monorail_issues = pool.map(
578 self.monorail_issue_search, monorail_projects.keys())
579 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
580
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700581 if not monorail_issues:
582 return
583
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100584 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
585 filtered_issues = pool.map(
586 self.filter_modified_monorail_issue, monorail_issues)
587 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100588
589 def get_referenced_issues(self):
590 if not self.issues:
591 self.get_issues()
592
593 if not self.changes:
594 self.get_changes()
595
596 referenced_issue_uids = set(itertools.chain.from_iterable(
597 change['bugs'] for change in self.changes))
598 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
599 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
600
601 missing_issues_by_project = collections.defaultdict(list)
602 for issue_uid in missing_issue_uids:
603 project, issue_id = issue_uid.split(':')
604 missing_issues_by_project[project].append(issue_id)
605
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000606 for project, issue_ids in missing_issues_by_project.items():
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100607 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000608
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000609 def print_issues(self):
610 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000611 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000612 for issue in self.issues:
613 self.print_issue(issue)
614
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100615 def print_changes_by_issue(self, skip_empty_own):
616 if not self.issues or not self.changes:
617 return
618
619 self.print_heading('Changes by referenced issue(s)')
620 issues = {issue['uid']: issue for issue in self.issues}
621 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
622 changes_by_issue_uid = collections.defaultdict(list)
623 changes_by_ref_issue_uid = collections.defaultdict(list)
624 changes_without_issue = []
625 for change in self.changes:
626 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000627 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100628 if issue_uid in issues:
629 changes_by_issue_uid[issue_uid].append(change)
630 added = True
631 if issue_uid in ref_issues:
632 changes_by_ref_issue_uid[issue_uid].append(change)
633 added = True
634 if not added:
635 changes_without_issue.append(change)
636
637 # Changes referencing own issues.
638 for issue_uid in issues:
639 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
640 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000641 if changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000642 print()
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000643 for change in changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000644 print(' ', end='') # this prints no newline
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000645 self.print_change(change)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000646 print()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100647
648 # Changes referencing others' issues.
649 for issue_uid in ref_issues:
650 assert changes_by_ref_issue_uid[issue_uid]
651 self.print_issue(ref_issues[issue_uid])
652 for change in changes_by_ref_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000653 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100654 self.print_change(change)
655
656 # Changes referencing no issues.
657 if changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000658 print(self.options.output_format_no_url.format(title='Other changes'))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100659 for change in changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000660 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100661 self.print_change(change)
662
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000663 def print_activity(self):
664 self.print_changes()
665 self.print_reviews()
666 self.print_issues()
667
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000668 def dump_json(self, ignore_keys=None):
669 if ignore_keys is None:
670 ignore_keys = ['replies']
671
672 def format_for_json_dump(in_array):
673 output = {}
674 for item in in_array:
675 url = item.get('url') or item.get('review_url')
676 if not url:
677 raise Exception('Dumped item %s does not specify url' % item)
678 output[url] = dict(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000679 (k, v) for k,v in item.items() if k not in ignore_keys)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000680 return output
681
682 class PythonObjectEncoder(json.JSONEncoder):
683 def default(self, obj): # pylint: disable=method-hidden
684 if isinstance(obj, datetime):
685 return obj.isoformat()
686 if isinstance(obj, set):
687 return list(obj)
688 return json.JSONEncoder.default(self, obj)
689
690 output = {
691 'reviews': format_for_json_dump(self.reviews),
692 'changes': format_for_json_dump(self.changes),
693 'issues': format_for_json_dump(self.issues)
694 }
Raul Tambre80ee78e2019-05-06 22:41:05 +0000695 print(json.dumps(output, indent=2, cls=PythonObjectEncoder))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000696
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000697
698def main():
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000699 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
700 parser.add_option(
701 '-u', '--user', metavar='<email>',
Bruce Dawson84370962019-02-21 05:33:37 +0000702 # Look for USER and USERNAME (Windows) environment variables.
703 default=os.environ.get('USER', os.environ.get('USERNAME')),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000704 help='Filter on user, default=%default')
705 parser.add_option(
706 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000707 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000708 parser.add_option(
709 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000710 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000711 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
712 relativedelta(months=2))
713 parser.add_option(
714 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000715 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000716 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
717 parser.add_option(
718 '-Y', '--this_year', action='store_true',
719 help='Use this year\'s dates')
720 parser.add_option(
721 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000722 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000723 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000724 '-W', '--last_week', action='count',
725 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000726 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000727 '-a', '--auth',
728 action='store_true',
729 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000730 parser.add_option(
731 '-d', '--deltas',
732 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800733 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100734 parser.add_option(
735 '--no-referenced-issues',
736 action='store_true',
737 help='Do not fetch issues referenced by owned changes. Useful in '
738 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100739 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100740 parser.add_option(
741 '--skip-own-issues-without-changes',
742 action='store_true',
743 help='Skips listing own issues without changes when showing changes '
744 'grouped by referenced issue(s). See --changes-by-issue for more '
745 'details.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000746
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000747 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000748 'By default, all activity will be looked up and '
749 'printed. If any of these are specified, only '
750 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000751 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000752 '-c', '--changes',
753 action='store_true',
754 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000755 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000756 '-i', '--issues',
757 action='store_true',
758 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000759 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000760 '-r', '--reviews',
761 action='store_true',
762 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100763 activity_types_group.add_option(
764 '--changes-by-issue', action='store_true',
765 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000766 parser.add_option_group(activity_types_group)
767
768 output_format_group = optparse.OptionGroup(parser, 'Output Format',
769 'By default, all activity will be printed in the '
770 'following format: {url} {title}. This can be '
771 'changed for either all activity types or '
772 'individually for each activity type. The format '
773 'is defined as documented for '
774 'string.format(...). The variables available for '
Lutz Justen860378e2019-03-12 01:10:02 +0000775 'all activity types are url, title, author, '
776 'created and modified. Format options for '
777 'specific activity types will override the '
778 'generic format.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000779 output_format_group.add_option(
780 '-f', '--output-format', metavar='<format>',
781 default=u'{url} {title}',
782 help='Specifies the format to use when printing all your activity.')
783 output_format_group.add_option(
784 '--output-format-changes', metavar='<format>',
785 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000786 help='Specifies the format to use when printing changes. Supports the '
787 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000788 output_format_group.add_option(
789 '--output-format-issues', metavar='<format>',
790 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000791 help='Specifies the format to use when printing issues. Supports the '
792 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000793 output_format_group.add_option(
794 '--output-format-reviews', metavar='<format>',
795 default=None,
796 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000797 output_format_group.add_option(
798 '--output-format-heading', metavar='<format>',
799 default=u'{heading}:',
800 help='Specifies the format to use when printing headings.')
801 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100802 '--output-format-no-url', default='{title}',
803 help='Specifies the format to use when printing activity without url.')
804 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000805 '-m', '--markdown', action='store_true',
806 help='Use markdown-friendly output (overrides --output-format '
807 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000808 output_format_group.add_option(
809 '-j', '--json', action='store_true',
810 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000811 parser.add_option_group(output_format_group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000812 auth.add_auth_options(parser)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000813
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000814 parser.add_option(
815 '-v', '--verbose',
816 action='store_const',
817 dest='verbosity',
818 default=logging.WARN,
819 const=logging.INFO,
820 help='Output extra informational messages.'
821 )
822 parser.add_option(
823 '-q', '--quiet',
824 action='store_const',
825 dest='verbosity',
826 const=logging.ERROR,
827 help='Suppress non-error messages.'
828 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000829 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000830 '-M', '--merged-only',
831 action='store_true',
832 dest='merged_only',
833 default=False,
834 help='Shows only changes that have been merged.')
835 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000836 '-C', '--completed-issues',
837 action='store_true',
838 dest='completed_issues',
839 default=False,
840 help='Shows only monorail issues that have completed (Fixed|Verified) '
841 'by the user.')
842 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000843 '-o', '--output', metavar='<file>',
844 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000845
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000846 # Remove description formatting
847 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800848 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000849
850 options, args = parser.parse_args()
851 options.local_user = os.environ.get('USER')
852 if args:
853 parser.error('Args unsupported')
854 if not options.user:
Bruce Dawson84370962019-02-21 05:33:37 +0000855 parser.error('USER/USERNAME is not set, please use -u')
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000856 # Retains the original -u option as the email address.
857 options.email = options.user
858 options.user = username(options.email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000859
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000860 logging.basicConfig(level=options.verbosity)
861
862 # python-keyring provides easy access to the system keyring.
863 try:
864 import keyring # pylint: disable=unused-import,unused-variable,F0401
865 except ImportError:
866 logging.warning('Consider installing python-keyring')
867
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000868 if not options.begin:
869 if options.last_quarter:
870 begin, end = quarter_begin, quarter_end
871 elif options.this_year:
872 begin, end = get_year_of(datetime.today())
873 elif options.week_of:
874 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000875 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000876 begin, end = (get_week_of(datetime.today() -
877 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000878 else:
879 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
880 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700881 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000882 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700883 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000884 else:
885 end = datetime.today()
886 options.begin, options.end = begin, end
Bruce Dawson2afcf222019-02-25 22:07:08 +0000887 if begin >= end:
888 # The queries fail in peculiar ways when the begin date is in the future.
889 # Give a descriptive error message instead.
890 logging.error('Start date (%s) is the same or later than end date (%s)' %
891 (begin, end))
892 return 1
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000893
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000894 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000895 options.output_format_heading = '### {heading}\n'
896 options.output_format = ' * [{title}]({url})'
897 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000898 logging.info('Searching for activity by %s', options.user)
899 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000900
901 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100902 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000903
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100904 if not (options.changes or options.reviews or options.issues or
905 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000906 options.changes = True
907 options.issues = True
908 options.reviews = True
909
910 # First do any required authentication so none of the user interaction has to
911 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100912 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000913 my_activity.auth_for_changes()
914 if options.reviews:
915 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000916
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000917 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000918
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000919 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100920 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000921 my_activity.get_changes()
922 if options.reviews:
923 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100924 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000925 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100926 if not options.no_referenced_issues:
927 my_activity.get_referenced_issues()
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000928 except auth.AuthenticationError as e:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000929 logging.error('auth.AuthenticationError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000930
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100931 my_activity.show_progress('\n')
932
Vadim Bendebury8de38002018-05-14 19:02:55 -0700933 my_activity.print_access_errors()
934
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000935 output_file = None
936 try:
937 if options.output:
938 output_file = open(options.output, 'w')
939 logging.info('Printing output to "%s"', options.output)
940 sys.stdout = output_file
941 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700942 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000943 else:
944 if options.json:
945 my_activity.dump_json()
946 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100947 if options.changes:
948 my_activity.print_changes()
949 if options.reviews:
950 my_activity.print_reviews()
951 if options.issues:
952 my_activity.print_issues()
953 if options.changes_by_issue:
954 my_activity.print_changes_by_issue(
955 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000956 finally:
957 if output_file:
958 logging.info('Done printing to file.')
959 sys.stdout = sys.__stdout__
960 output_file.close()
961
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000962 return 0
963
964
965if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000966 # Fix encoding to support non-ascii issue titles.
967 fix_encoding.fix_encoding()
968
sbc@chromium.org013731e2015-02-26 18:28:43 +0000969 try:
970 sys.exit(main())
971 except KeyboardInterrupt:
972 sys.stderr.write('interrupted\n')
973 sys.exit(1)