blob: 5e76cc15fbd840d0607892053c266258600fc060 [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)
296 authenticator = auth.get_authenticator_for_host(
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000297 'bugs.chromium.org', auth_config)
Kenneth Russell66badbd2018-09-09 21:35:32 +0000298 # Manually use a long timeout (10m); for some users who have a
299 # long history on the issue tracker, whatever the default timeout
300 # is is reached.
301 return authenticator.authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100302
303 def filter_modified_monorail_issue(self, issue):
304 """Precisely checks if an issue has been modified in the time range.
305
306 This fetches all issue comments to check if the issue has been modified in
307 the time range specified by user. This is needed because monorail only
308 allows filtering by last updated and published dates, which is not
309 sufficient to tell whether a given issue has been modified at some specific
310 time range. Any update to the issue is a reported as comment on Monorail.
311
312 Args:
313 issue: Issue dict as returned by monorail_query_issues method. In
314 particular, must have a key 'uid' formatted as 'project:issue_id'.
315
316 Returns:
317 Passed issue if modified, None otherwise.
318 """
319 http = self.monorail_get_auth_http()
320 project, issue_id = issue['uid'].split(':')
321 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
322 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
323 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100324 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100325 content = json.loads(body)
326 if not content:
327 logging.error('Unable to parse %s response from monorail.', project)
328 return issue
329
330 for item in content.get('items', []):
331 comment_published = datetime_from_monorail(item['published'])
332 if self.filter_modified(comment_published):
333 return issue
334
335 return None
336
337 def monorail_query_issues(self, project, query):
338 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000339 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100340 '/%s/issues') % project
341 query_data = urllib.urlencode(query)
342 url = url + '?' + query_data
343 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100344 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100345 content = json.loads(body)
346 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100347 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100348 return []
349
350 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100351 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100352 for item in content.get('items', []):
353 if project_config.get('shorturl'):
354 protocol = project_config.get('short_url_protocol', 'http')
355 item_url = '%s://%s/%d' % (
356 protocol, project_config['shorturl'], item['id'])
357 else:
358 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
359 project, item['id'])
360 issue = {
361 'uid': '%s:%s' % (project, item['id']),
362 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100363 'created': datetime_from_monorail(item['published']),
364 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100365 'author': item['author']['name'],
366 'url': item_url,
367 'comments': [],
368 'status': item['status'],
369 'labels': [],
370 'components': []
371 }
372 if 'owner' in item:
373 issue['owner'] = item['owner']['name']
374 else:
375 issue['owner'] = 'None'
376 if 'labels' in item:
377 issue['labels'] = item['labels']
378 if 'components' in item:
379 issue['components'] = item['components']
380 issues.append(issue)
381
382 return issues
383
384 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000385 epoch = datetime.utcfromtimestamp(0)
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000386 # Defaults to @chromium.org email if one wasn't provided on -u option.
387 user_str = (self.options.email if self.options.email.find('@') >= 0
388 else '%s@chromium.org' % self.user)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000389
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100390 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000391 'maxResults': 10000,
392 'q': user_str,
393 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
394 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000395 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000396
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000397 if self.options.completed_issues:
398 return [
399 issue for issue in issues
400 if (self.match(issue['owner']) and
401 issue['status'].lower() in ('verified', 'fixed'))
402 ]
403
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100404 return [
405 issue for issue in issues
406 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000407
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100408 def monorail_get_issues(self, project, issue_ids):
409 return self.monorail_query_issues(project, {
410 'maxResults': 10000,
411 'q': 'id:%s' % ','.join(issue_ids)
412 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000413
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000414 def print_heading(self, heading):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000415 print()
416 print(self.options.output_format_heading.format(heading=heading))
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000417
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000418 def match(self, author):
419 if '@' in self.user:
420 return author == self.user
421 return author.startswith(self.user + '@')
422
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000423 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000424 activity = len([
425 reply
426 for reply in change['replies']
427 if self.match(reply['author'])
428 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000429 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000430 'created': change['created'].date().isoformat(),
431 'modified': change['modified'].date().isoformat(),
432 'reviewers': ', '.join(change['reviewers']),
433 'status': change['status'],
434 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000435 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000436 if self.options.deltas:
437 optional_values['delta'] = change['delta']
438
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000439 self.print_generic(self.options.output_format,
440 self.options.output_format_changes,
441 change['header'],
442 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000443 change['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000444 change['created'],
445 change['modified'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000446 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000447
448 def print_issue(self, issue):
449 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000450 'created': issue['created'].date().isoformat(),
451 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000452 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000453 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000454 }
455 self.print_generic(self.options.output_format,
456 self.options.output_format_issues,
457 issue['header'],
458 issue['url'],
459 issue['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000460 issue['created'],
461 issue['modified'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000462 optional_values)
463
464 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000465 activity = len([
466 reply
467 for reply in review['replies']
468 if self.match(reply['author'])
469 ])
470 optional_values = {
471 'created': review['created'].date().isoformat(),
472 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800473 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000474 'activity': activity,
475 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800476 if self.options.deltas:
477 optional_values['delta'] = review['delta']
478
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000479 self.print_generic(self.options.output_format,
480 self.options.output_format_reviews,
481 review['header'],
482 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000483 review['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000484 review['created'],
485 review['modified'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000486 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000487
488 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000489 def print_generic(default_fmt, specific_fmt,
Lutz Justen860378e2019-03-12 01:10:02 +0000490 title, url, author, created, modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000491 optional_values=None):
492 output_format = specific_fmt if specific_fmt is not None else default_fmt
493 output_format = unicode(output_format)
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000494 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000495 'title': title,
496 'url': url,
497 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000498 'created': created,
499 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000500 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000501 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000502 values.update(optional_values)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000503 print(DefaultFormatter().format(output_format,
504 **values).encode(sys.getdefaultencoding()))
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000505
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000506
507 def filter_issue(self, issue, should_filter_by_user=True):
508 def maybe_filter_username(email):
509 return not should_filter_by_user or username(email) == self.user
510 if (maybe_filter_username(issue['author']) and
511 self.filter_modified(issue['created'])):
512 return True
513 if (maybe_filter_username(issue['owner']) and
514 (self.filter_modified(issue['created']) or
515 self.filter_modified(issue['modified']))):
516 return True
517 for reply in issue['replies']:
518 if self.filter_modified(reply['created']):
519 if not should_filter_by_user:
520 break
521 if (username(reply['author']) == self.user
522 or (self.user + '@') in reply['content']):
523 break
524 else:
525 return False
526 return True
527
528 def filter_modified(self, modified):
529 return self.modified_after < modified and modified < self.modified_before
530
531 def auth_for_changes(self):
532 #TODO(cjhopman): Move authentication check for getting changes here.
533 pass
534
535 def auth_for_reviews(self):
536 # Reviews use all the same instances as changes so no authentication is
537 # required.
538 pass
539
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000540 def get_changes(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000541 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100542 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100543 gerrit_changes = pool.map_async(
544 lambda instance: self.gerrit_search(instance, owner=self.user),
545 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100546 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000547 self.changes = list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000548
549 def print_changes(self):
550 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000551 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000552 for change in self.changes:
Bruce Dawson2afcf222019-02-25 22:07:08 +0000553 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000554
Vadim Bendebury8de38002018-05-14 19:02:55 -0700555 def print_access_errors(self):
556 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400557 logging.error('Access Errors:')
558 for error in self.access_errors:
559 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700560
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000561 def get_reviews(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000562 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100563 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100564 gerrit_reviews = pool.map_async(
565 lambda instance: self.gerrit_search(instance, reviewer=self.user),
566 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100567 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000568 self.reviews = list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000569
570 def print_reviews(self):
571 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000572 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000573 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000574 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000575
576 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100577 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
578 monorail_issues = pool.map(
579 self.monorail_issue_search, monorail_projects.keys())
580 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
581
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700582 if not monorail_issues:
583 return
584
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100585 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
586 filtered_issues = pool.map(
587 self.filter_modified_monorail_issue, monorail_issues)
588 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100589
590 def get_referenced_issues(self):
591 if not self.issues:
592 self.get_issues()
593
594 if not self.changes:
595 self.get_changes()
596
597 referenced_issue_uids = set(itertools.chain.from_iterable(
598 change['bugs'] for change in self.changes))
599 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
600 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
601
602 missing_issues_by_project = collections.defaultdict(list)
603 for issue_uid in missing_issue_uids:
604 project, issue_id = issue_uid.split(':')
605 missing_issues_by_project[project].append(issue_id)
606
607 for project, issue_ids in missing_issues_by_project.iteritems():
608 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000609
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000610 def print_issues(self):
611 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000612 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000613 for issue in self.issues:
614 self.print_issue(issue)
615
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100616 def print_changes_by_issue(self, skip_empty_own):
617 if not self.issues or not self.changes:
618 return
619
620 self.print_heading('Changes by referenced issue(s)')
621 issues = {issue['uid']: issue for issue in self.issues}
622 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
623 changes_by_issue_uid = collections.defaultdict(list)
624 changes_by_ref_issue_uid = collections.defaultdict(list)
625 changes_without_issue = []
626 for change in self.changes:
627 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000628 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100629 if issue_uid in issues:
630 changes_by_issue_uid[issue_uid].append(change)
631 added = True
632 if issue_uid in ref_issues:
633 changes_by_ref_issue_uid[issue_uid].append(change)
634 added = True
635 if not added:
636 changes_without_issue.append(change)
637
638 # Changes referencing own issues.
639 for issue_uid in issues:
640 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
641 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000642 if changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000643 print()
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000644 for change in changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000645 print(' ', end='') # this prints no newline
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000646 self.print_change(change)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000647 print()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100648
649 # Changes referencing others' issues.
650 for issue_uid in ref_issues:
651 assert changes_by_ref_issue_uid[issue_uid]
652 self.print_issue(ref_issues[issue_uid])
653 for change in changes_by_ref_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000654 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100655 self.print_change(change)
656
657 # Changes referencing no issues.
658 if changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000659 print(self.options.output_format_no_url.format(title='Other changes'))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100660 for change in changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000661 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100662 self.print_change(change)
663
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000664 def print_activity(self):
665 self.print_changes()
666 self.print_reviews()
667 self.print_issues()
668
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000669 def dump_json(self, ignore_keys=None):
670 if ignore_keys is None:
671 ignore_keys = ['replies']
672
673 def format_for_json_dump(in_array):
674 output = {}
675 for item in in_array:
676 url = item.get('url') or item.get('review_url')
677 if not url:
678 raise Exception('Dumped item %s does not specify url' % item)
679 output[url] = dict(
680 (k, v) for k,v in item.iteritems() if k not in ignore_keys)
681 return output
682
683 class PythonObjectEncoder(json.JSONEncoder):
684 def default(self, obj): # pylint: disable=method-hidden
685 if isinstance(obj, datetime):
686 return obj.isoformat()
687 if isinstance(obj, set):
688 return list(obj)
689 return json.JSONEncoder.default(self, obj)
690
691 output = {
692 'reviews': format_for_json_dump(self.reviews),
693 'changes': format_for_json_dump(self.changes),
694 'issues': format_for_json_dump(self.issues)
695 }
Raul Tambre80ee78e2019-05-06 22:41:05 +0000696 print(json.dumps(output, indent=2, cls=PythonObjectEncoder))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000697
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000698
699def main():
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000700 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
701 parser.add_option(
702 '-u', '--user', metavar='<email>',
Bruce Dawson84370962019-02-21 05:33:37 +0000703 # Look for USER and USERNAME (Windows) environment variables.
704 default=os.environ.get('USER', os.environ.get('USERNAME')),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000705 help='Filter on user, default=%default')
706 parser.add_option(
707 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000708 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000709 parser.add_option(
710 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000711 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000712 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
713 relativedelta(months=2))
714 parser.add_option(
715 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000716 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000717 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
718 parser.add_option(
719 '-Y', '--this_year', action='store_true',
720 help='Use this year\'s dates')
721 parser.add_option(
722 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000723 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000724 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000725 '-W', '--last_week', action='count',
726 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000727 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000728 '-a', '--auth',
729 action='store_true',
730 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000731 parser.add_option(
732 '-d', '--deltas',
733 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800734 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100735 parser.add_option(
736 '--no-referenced-issues',
737 action='store_true',
738 help='Do not fetch issues referenced by owned changes. Useful in '
739 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100740 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100741 parser.add_option(
742 '--skip-own-issues-without-changes',
743 action='store_true',
744 help='Skips listing own issues without changes when showing changes '
745 'grouped by referenced issue(s). See --changes-by-issue for more '
746 'details.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000747
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000748 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000749 'By default, all activity will be looked up and '
750 'printed. If any of these are specified, only '
751 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000752 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000753 '-c', '--changes',
754 action='store_true',
755 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000756 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000757 '-i', '--issues',
758 action='store_true',
759 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000760 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000761 '-r', '--reviews',
762 action='store_true',
763 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100764 activity_types_group.add_option(
765 '--changes-by-issue', action='store_true',
766 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000767 parser.add_option_group(activity_types_group)
768
769 output_format_group = optparse.OptionGroup(parser, 'Output Format',
770 'By default, all activity will be printed in the '
771 'following format: {url} {title}. This can be '
772 'changed for either all activity types or '
773 'individually for each activity type. The format '
774 'is defined as documented for '
775 'string.format(...). The variables available for '
Lutz Justen860378e2019-03-12 01:10:02 +0000776 'all activity types are url, title, author, '
777 'created and modified. Format options for '
778 'specific activity types will override the '
779 'generic format.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000780 output_format_group.add_option(
781 '-f', '--output-format', metavar='<format>',
782 default=u'{url} {title}',
783 help='Specifies the format to use when printing all your activity.')
784 output_format_group.add_option(
785 '--output-format-changes', metavar='<format>',
786 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000787 help='Specifies the format to use when printing changes. Supports the '
788 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000789 output_format_group.add_option(
790 '--output-format-issues', metavar='<format>',
791 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000792 help='Specifies the format to use when printing issues. Supports the '
793 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000794 output_format_group.add_option(
795 '--output-format-reviews', metavar='<format>',
796 default=None,
797 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000798 output_format_group.add_option(
799 '--output-format-heading', metavar='<format>',
800 default=u'{heading}:',
801 help='Specifies the format to use when printing headings.')
802 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100803 '--output-format-no-url', default='{title}',
804 help='Specifies the format to use when printing activity without url.')
805 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000806 '-m', '--markdown', action='store_true',
807 help='Use markdown-friendly output (overrides --output-format '
808 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000809 output_format_group.add_option(
810 '-j', '--json', action='store_true',
811 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000812 parser.add_option_group(output_format_group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000813 auth.add_auth_options(parser)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000814
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000815 parser.add_option(
816 '-v', '--verbose',
817 action='store_const',
818 dest='verbosity',
819 default=logging.WARN,
820 const=logging.INFO,
821 help='Output extra informational messages.'
822 )
823 parser.add_option(
824 '-q', '--quiet',
825 action='store_const',
826 dest='verbosity',
827 const=logging.ERROR,
828 help='Suppress non-error messages.'
829 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000830 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000831 '-M', '--merged-only',
832 action='store_true',
833 dest='merged_only',
834 default=False,
835 help='Shows only changes that have been merged.')
836 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000837 '-C', '--completed-issues',
838 action='store_true',
839 dest='completed_issues',
840 default=False,
841 help='Shows only monorail issues that have completed (Fixed|Verified) '
842 'by the user.')
843 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000844 '-o', '--output', metavar='<file>',
845 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000846
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000847 # Remove description formatting
848 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800849 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000850
851 options, args = parser.parse_args()
852 options.local_user = os.environ.get('USER')
853 if args:
854 parser.error('Args unsupported')
855 if not options.user:
Bruce Dawson84370962019-02-21 05:33:37 +0000856 parser.error('USER/USERNAME is not set, please use -u')
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000857 # Retains the original -u option as the email address.
858 options.email = options.user
859 options.user = username(options.email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000860
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000861 logging.basicConfig(level=options.verbosity)
862
863 # python-keyring provides easy access to the system keyring.
864 try:
865 import keyring # pylint: disable=unused-import,unused-variable,F0401
866 except ImportError:
867 logging.warning('Consider installing python-keyring')
868
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000869 if not options.begin:
870 if options.last_quarter:
871 begin, end = quarter_begin, quarter_end
872 elif options.this_year:
873 begin, end = get_year_of(datetime.today())
874 elif options.week_of:
875 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000876 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000877 begin, end = (get_week_of(datetime.today() -
878 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000879 else:
880 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
881 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700882 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000883 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700884 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000885 else:
886 end = datetime.today()
887 options.begin, options.end = begin, end
Bruce Dawson2afcf222019-02-25 22:07:08 +0000888 if begin >= end:
889 # The queries fail in peculiar ways when the begin date is in the future.
890 # Give a descriptive error message instead.
891 logging.error('Start date (%s) is the same or later than end date (%s)' %
892 (begin, end))
893 return 1
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000894
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000895 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000896 options.output_format_heading = '### {heading}\n'
897 options.output_format = ' * [{title}]({url})'
898 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000899 logging.info('Searching for activity by %s', options.user)
900 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000901
902 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100903 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000904
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100905 if not (options.changes or options.reviews or options.issues or
906 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000907 options.changes = True
908 options.issues = True
909 options.reviews = True
910
911 # First do any required authentication so none of the user interaction has to
912 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100913 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000914 my_activity.auth_for_changes()
915 if options.reviews:
916 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000917
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000918 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000919
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000920 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100921 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000922 my_activity.get_changes()
923 if options.reviews:
924 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100925 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000926 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100927 if not options.no_referenced_issues:
928 my_activity.get_referenced_issues()
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000929 except auth.AuthenticationError as e:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000930 logging.error('auth.AuthenticationError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000931
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100932 my_activity.show_progress('\n')
933
Vadim Bendebury8de38002018-05-14 19:02:55 -0700934 my_activity.print_access_errors()
935
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000936 output_file = None
937 try:
938 if options.output:
939 output_file = open(options.output, 'w')
940 logging.info('Printing output to "%s"', options.output)
941 sys.stdout = output_file
942 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700943 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000944 else:
945 if options.json:
946 my_activity.dump_json()
947 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100948 if options.changes:
949 my_activity.print_changes()
950 if options.reviews:
951 my_activity.print_reviews()
952 if options.issues:
953 my_activity.print_issues()
954 if options.changes_by_issue:
955 my_activity.print_changes_by_issue(
956 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000957 finally:
958 if output_file:
959 logging.info('Done printing to file.')
960 sys.stdout = sys.__stdout__
961 output_file.close()
962
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000963 return 0
964
965
966if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000967 # Fix encoding to support non-ascii issue titles.
968 fix_encoding.fix_encoding()
969
sbc@chromium.org013731e2015-02-26 18:28:43 +0000970 try:
971 sys.exit(main())
972 except KeyboardInterrupt:
973 sys.stderr.write('interrupted\n')
974 sys.exit(1)