blob: 21e409d232186aa31e54da59866f39189ffd69bc [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
41from functools import partial
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010042import itertools
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000043import json
Tobias Sargeantffb3c432017-03-08 14:09:14 +000044import logging
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +010045from multiprocessing.pool import ThreadPool
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000046import optparse
47import os
48import subprocess
Tobias Sargeantffb3c432017-03-08 14:09:14 +000049from string import Formatter
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000050import sys
51import urllib
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +000052import re
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000053
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000054import auth
stevefung@chromium.org832d51e2015-05-27 12:52:51 +000055import fix_encoding
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000056import gerrit_util
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000057
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +000058from third_party import httplib2
59
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000060try:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000061 import dateutil # pylint: disable=import-error
62 import dateutil.parser
63 from dateutil.relativedelta import relativedelta
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000064except ImportError:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000065 logging.error('python-dateutil package required')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000066 exit(1)
67
Tobias Sargeantffb3c432017-03-08 14:09:14 +000068
69class DefaultFormatter(Formatter):
70 def __init__(self, default = ''):
71 super(DefaultFormatter, self).__init__()
72 self.default = default
73
74 def get_value(self, key, args, kwds):
75 if isinstance(key, basestring) and key not in kwds:
76 return self.default
77 return Formatter.get_value(self, key, args, kwds)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000078
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000079gerrit_instances = [
80 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000081 'url': 'android-review.googlesource.com',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000082 },
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000083 {
84 'url': 'chrome-internal-review.googlesource.com',
Jeremy Romanf475a472017-06-06 16:49:11 -040085 'shorturl': 'crrev.com/i',
Varun Khanejad9f97bc2017-08-02 12:55:01 -070086 'short_url_protocol': 'https',
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000087 },
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000088 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000089 'url': 'chromium-review.googlesource.com',
90 'shorturl': 'crrev.com/c',
91 'short_url_protocol': 'https',
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000092 },
Ryan Harrison897602a2017-09-18 16:23:41 -040093 {
Ryan Harrison06e18692019-09-23 18:22:25 +000094 'url': 'dawn-review.googlesource.com',
95 },
96 {
Ryan Harrison897602a2017-09-18 16:23:41 -040097 'url': 'pdfium-review.googlesource.com',
98 },
Adrienne Walker95d4c852018-09-27 20:28:12 +000099 {
100 'url': 'skia-review.googlesource.com',
101 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000102]
103
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100104monorail_projects = {
Jamie Madill1db68ea2019-09-03 19:34:42 +0000105 'angleproject': {
106 'shorturl': 'anglebug.com',
107 'short_url_protocol': 'http',
108 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100109 'chromium': {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000110 'shorturl': 'crbug.com',
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700111 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000112 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000113 'dawn': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100114 'google-breakpad': {},
115 'gyp': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100116 'pdfium': {
Ryan Harrison897602a2017-09-18 16:23:41 -0400117 'shorturl': 'crbug.com/pdfium',
118 'short_url_protocol': 'https',
119 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000120 'skia': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100121 'v8': {
122 'shorturl': 'crbug.com/v8',
123 'short_url_protocol': 'https',
124 },
125}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000126
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000127def username(email):
128 """Keeps the username of an email address."""
129 return email and email.split('@', 1)[0]
130
131
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000132def datetime_to_midnight(date):
133 return date - timedelta(hours=date.hour, minutes=date.minute,
134 seconds=date.second, microseconds=date.microsecond)
135
136
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000137def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000138 begin = (datetime_to_midnight(date) -
139 relativedelta(months=(date.month % 3) - 1, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000140 return begin, begin + relativedelta(months=3)
141
142
143def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000144 begin = (datetime_to_midnight(date) -
145 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000146 return begin, begin + relativedelta(years=1)
147
148
149def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000150 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000151 return begin, begin + timedelta(days=7)
152
153
154def get_yes_or_no(msg):
155 while True:
156 response = raw_input(msg + ' yes/no [no] ')
157 if response == 'y' or response == 'yes':
158 return True
159 elif not response or response == 'n' or response == 'no':
160 return False
161
162
deymo@chromium.org6c039202013-09-12 12:28:12 +0000163def datetime_from_gerrit(date_string):
164 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
165
166
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100167def datetime_from_monorail(date_string):
168 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000169
170
171class MyActivity(object):
172 def __init__(self, options):
173 self.options = options
174 self.modified_after = options.begin
175 self.modified_before = options.end
176 self.user = options.user
177 self.changes = []
178 self.reviews = []
179 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100180 self.referenced_issues = []
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000181 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-14 19:02:55 -0700182 self.access_errors = set()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000183
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100184 def show_progress(self, how='.'):
185 if sys.stdout.isatty():
186 sys.stdout.write(how)
187 sys.stdout.flush()
188
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000189 def extract_bug_numbers_from_description(self, issue):
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000190 description = None
191
192 if 'description' in issue:
193 # Getting the description for Rietveld
194 description = issue['description']
195 elif 'revisions' in issue:
196 # Getting the description for REST Gerrit
197 revision = issue['revisions'][issue['current_revision']]
198 description = revision['commit']['message']
199
200 bugs = []
201 if description:
Nicolas Dossou-gbete903ea732017-07-10 16:46:59 +0100202 # Handle both "Bug: 99999" and "BUG=99999" bug notations
203 # Multiple bugs can be noted on a single line or in multiple ones.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100204 matches = re.findall(
205 r'BUG[=:]\s?((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)', description,
206 flags=re.IGNORECASE)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000207 if matches:
208 for match in matches:
209 bugs.extend(match[0].replace(' ', '').split(','))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100210 # Add default chromium: prefix if none specified.
211 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000212
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000213 return sorted(set(bugs))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000214
Vadim Bendebury8de38002018-05-14 19:02:55 -0700215 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200216 # Convert the "key:value" filter to a list of (key, value) pairs.
217 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000218 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000219 # Instantiate the generator to force all the requests now and catch the
220 # errors here.
221 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000222 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
223 'CURRENT_REVISION', 'CURRENT_COMMIT']))
Raul Tambre7c938462019-05-24 16:35:35 +0000224 except gerrit_util.GerritError as e:
Vadim Bendebury8de38002018-05-14 19:02:55 -0700225 error_message = 'Looking up %r: %s' % (instance['url'], e)
226 if error_message not in self.access_errors:
227 self.access_errors.add(error_message)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000228 return []
229
deymo@chromium.org6c039202013-09-12 12:28:12 +0000230 def gerrit_search(self, instance, owner=None, reviewer=None):
231 max_age = datetime.today() - self.modified_after
Andrii Shyshkalov4aedec02018-09-17 16:31:17 +0000232 filters = ['-age:%ss' % (max_age.days * 24 * 3600 + max_age.seconds)]
233 if owner:
234 assert not reviewer
235 filters.append('owner:%s' % owner)
236 else:
237 filters.extend(('-owner:%s' % reviewer, 'reviewer:%s' % reviewer))
Peter K. Lee9342ac02018-08-28 21:03:51 +0000238 # TODO(cjhopman): Should abandoned changes be filtered out when
239 # merged_only is not enabled?
240 if self.options.merged_only:
241 filters.append('status:merged')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000242
Aaron Gable2979a872017-09-05 17:38:32 -0700243 issues = self.gerrit_changes_over_rest(instance, filters)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100244 self.show_progress()
Aaron Gable2979a872017-09-05 17:38:32 -0700245 issues = [self.process_gerrit_issue(instance, issue)
246 for issue in issues]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000247
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000248 issues = filter(self.filter_issue, issues)
249 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
250
251 return issues
252
Aaron Gable2979a872017-09-05 17:38:32 -0700253 def process_gerrit_issue(self, instance, issue):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000254 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000255 if self.options.deltas:
256 ret['delta'] = DefaultFormatter().format(
257 '+{insertions},-{deletions}',
258 **issue)
259 ret['status'] = issue['status']
deymo@chromium.org6c039202013-09-12 12:28:12 +0000260 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700261 protocol = instance.get('short_url_protocol', 'http')
262 url = instance['shorturl']
263 else:
264 protocol = 'https'
265 url = instance['url']
266 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
267
deymo@chromium.org6c039202013-09-12 12:28:12 +0000268 ret['header'] = issue['subject']
Don Garrett2ebf9fd2018-08-06 15:14:00 +0000269 ret['owner'] = issue['owner'].get('email', '')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000270 ret['author'] = ret['owner']
271 ret['created'] = datetime_from_gerrit(issue['created'])
272 ret['modified'] = datetime_from_gerrit(issue['updated'])
273 if 'messages' in issue:
Aaron Gable2979a872017-09-05 17:38:32 -0700274 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
deymo@chromium.org6c039202013-09-12 12:28:12 +0000275 else:
276 ret['replies'] = []
277 ret['reviewers'] = set(r['author'] for r in ret['replies'])
278 ret['reviewers'].discard(ret['author'])
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000279 ret['bugs'] = self.extract_bug_numbers_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000280 return ret
281
282 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700283 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000284 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000285 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
286 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000287 for reply in replies:
288 ret.append({
289 'author': reply['author']['email'],
290 'created': datetime_from_gerrit(reply['date']),
291 'content': reply['message'],
292 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000293 return ret
294
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100295 def monorail_get_auth_http(self):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000296 auth_config = auth.extract_auth_config_from_options(self.options)
297 authenticator = auth.get_authenticator_for_host(
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000298 'bugs.chromium.org', auth_config)
Kenneth Russell66badbd2018-09-09 21:35:32 +0000299 # Manually use a long timeout (10m); for some users who have a
300 # long history on the issue tracker, whatever the default timeout
301 # is is reached.
302 return authenticator.authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100303
304 def filter_modified_monorail_issue(self, issue):
305 """Precisely checks if an issue has been modified in the time range.
306
307 This fetches all issue comments to check if the issue has been modified in
308 the time range specified by user. This is needed because monorail only
309 allows filtering by last updated and published dates, which is not
310 sufficient to tell whether a given issue has been modified at some specific
311 time range. Any update to the issue is a reported as comment on Monorail.
312
313 Args:
314 issue: Issue dict as returned by monorail_query_issues method. In
315 particular, must have a key 'uid' formatted as 'project:issue_id'.
316
317 Returns:
318 Passed issue if modified, None otherwise.
319 """
320 http = self.monorail_get_auth_http()
321 project, issue_id = issue['uid'].split(':')
322 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
323 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
324 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100325 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100326 content = json.loads(body)
327 if not content:
328 logging.error('Unable to parse %s response from monorail.', project)
329 return issue
330
331 for item in content.get('items', []):
332 comment_published = datetime_from_monorail(item['published'])
333 if self.filter_modified(comment_published):
334 return issue
335
336 return None
337
338 def monorail_query_issues(self, project, query):
339 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000340 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100341 '/%s/issues') % project
342 query_data = urllib.urlencode(query)
343 url = url + '?' + query_data
344 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100345 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100346 content = json.loads(body)
347 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100348 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100349 return []
350
351 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100352 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100353 for item in content.get('items', []):
354 if project_config.get('shorturl'):
355 protocol = project_config.get('short_url_protocol', 'http')
356 item_url = '%s://%s/%d' % (
357 protocol, project_config['shorturl'], item['id'])
358 else:
359 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
360 project, item['id'])
361 issue = {
362 'uid': '%s:%s' % (project, item['id']),
363 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100364 'created': datetime_from_monorail(item['published']),
365 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100366 'author': item['author']['name'],
367 'url': item_url,
368 'comments': [],
369 'status': item['status'],
370 'labels': [],
371 'components': []
372 }
373 if 'owner' in item:
374 issue['owner'] = item['owner']['name']
375 else:
376 issue['owner'] = 'None'
377 if 'labels' in item:
378 issue['labels'] = item['labels']
379 if 'components' in item:
380 issue['components'] = item['components']
381 issues.append(issue)
382
383 return issues
384
385 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000386 epoch = datetime.utcfromtimestamp(0)
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000387 # Defaults to @chromium.org email if one wasn't provided on -u option.
388 user_str = (self.options.email if self.options.email.find('@') >= 0
389 else '%s@chromium.org' % self.user)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000390
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100391 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000392 'maxResults': 10000,
393 'q': user_str,
394 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
395 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000396 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000397
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000398 if self.options.completed_issues:
399 return [
400 issue for issue in issues
401 if (self.match(issue['owner']) and
402 issue['status'].lower() in ('verified', 'fixed'))
403 ]
404
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100405 return [
406 issue for issue in issues
407 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000408
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100409 def monorail_get_issues(self, project, issue_ids):
410 return self.monorail_query_issues(project, {
411 'maxResults': 10000,
412 'q': 'id:%s' % ','.join(issue_ids)
413 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000414
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000415 def print_heading(self, heading):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000416 print()
417 print(self.options.output_format_heading.format(heading=heading))
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000418
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000419 def match(self, author):
420 if '@' in self.user:
421 return author == self.user
422 return author.startswith(self.user + '@')
423
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000424 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000425 activity = len([
426 reply
427 for reply in change['replies']
428 if self.match(reply['author'])
429 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000430 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000431 'created': change['created'].date().isoformat(),
432 'modified': change['modified'].date().isoformat(),
433 'reviewers': ', '.join(change['reviewers']),
434 'status': change['status'],
435 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000436 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000437 if self.options.deltas:
438 optional_values['delta'] = change['delta']
439
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000440 self.print_generic(self.options.output_format,
441 self.options.output_format_changes,
442 change['header'],
443 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000444 change['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000445 change['created'],
446 change['modified'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000447 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000448
449 def print_issue(self, issue):
450 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000451 'created': issue['created'].date().isoformat(),
452 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000453 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000454 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000455 }
456 self.print_generic(self.options.output_format,
457 self.options.output_format_issues,
458 issue['header'],
459 issue['url'],
460 issue['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000461 issue['created'],
462 issue['modified'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000463 optional_values)
464
465 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000466 activity = len([
467 reply
468 for reply in review['replies']
469 if self.match(reply['author'])
470 ])
471 optional_values = {
472 'created': review['created'].date().isoformat(),
473 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800474 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000475 'activity': activity,
476 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800477 if self.options.deltas:
478 optional_values['delta'] = review['delta']
479
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000480 self.print_generic(self.options.output_format,
481 self.options.output_format_reviews,
482 review['header'],
483 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000484 review['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000485 review['created'],
486 review['modified'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000487 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000488
489 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000490 def print_generic(default_fmt, specific_fmt,
Lutz Justen860378e2019-03-12 01:10:02 +0000491 title, url, author, created, modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000492 optional_values=None):
493 output_format = specific_fmt if specific_fmt is not None else default_fmt
494 output_format = unicode(output_format)
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000495 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000496 'title': title,
497 'url': url,
498 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000499 'created': created,
500 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000501 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000502 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000503 values.update(optional_values)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000504 print(DefaultFormatter().format(output_format,
505 **values).encode(sys.getdefaultencoding()))
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000506
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000507
508 def filter_issue(self, issue, should_filter_by_user=True):
509 def maybe_filter_username(email):
510 return not should_filter_by_user or username(email) == self.user
511 if (maybe_filter_username(issue['author']) and
512 self.filter_modified(issue['created'])):
513 return True
514 if (maybe_filter_username(issue['owner']) and
515 (self.filter_modified(issue['created']) or
516 self.filter_modified(issue['modified']))):
517 return True
518 for reply in issue['replies']:
519 if self.filter_modified(reply['created']):
520 if not should_filter_by_user:
521 break
522 if (username(reply['author']) == self.user
523 or (self.user + '@') in reply['content']):
524 break
525 else:
526 return False
527 return True
528
529 def filter_modified(self, modified):
530 return self.modified_after < modified and modified < self.modified_before
531
532 def auth_for_changes(self):
533 #TODO(cjhopman): Move authentication check for getting changes here.
534 pass
535
536 def auth_for_reviews(self):
537 # Reviews use all the same instances as changes so no authentication is
538 # required.
539 pass
540
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000541 def get_changes(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000542 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100543 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100544 gerrit_changes = pool.map_async(
545 lambda instance: self.gerrit_search(instance, owner=self.user),
546 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100547 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000548 self.changes = list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000549
550 def print_changes(self):
551 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000552 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000553 for change in self.changes:
Bruce Dawson2afcf222019-02-25 22:07:08 +0000554 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000555
Vadim Bendebury8de38002018-05-14 19:02:55 -0700556 def print_access_errors(self):
557 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400558 logging.error('Access Errors:')
559 for error in self.access_errors:
560 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700561
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000562 def get_reviews(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000563 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100564 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100565 gerrit_reviews = pool.map_async(
566 lambda instance: self.gerrit_search(instance, reviewer=self.user),
567 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100568 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000569 self.reviews = list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000570
571 def print_reviews(self):
572 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000573 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000574 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000575 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000576
577 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100578 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
579 monorail_issues = pool.map(
580 self.monorail_issue_search, monorail_projects.keys())
581 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
582
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700583 if not monorail_issues:
584 return
585
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100586 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
587 filtered_issues = pool.map(
588 self.filter_modified_monorail_issue, monorail_issues)
589 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100590
591 def get_referenced_issues(self):
592 if not self.issues:
593 self.get_issues()
594
595 if not self.changes:
596 self.get_changes()
597
598 referenced_issue_uids = set(itertools.chain.from_iterable(
599 change['bugs'] for change in self.changes))
600 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
601 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
602
603 missing_issues_by_project = collections.defaultdict(list)
604 for issue_uid in missing_issue_uids:
605 project, issue_id = issue_uid.split(':')
606 missing_issues_by_project[project].append(issue_id)
607
608 for project, issue_ids in missing_issues_by_project.iteritems():
609 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000610
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000611 def print_issues(self):
612 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000613 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000614 for issue in self.issues:
615 self.print_issue(issue)
616
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100617 def print_changes_by_issue(self, skip_empty_own):
618 if not self.issues or not self.changes:
619 return
620
621 self.print_heading('Changes by referenced issue(s)')
622 issues = {issue['uid']: issue for issue in self.issues}
623 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
624 changes_by_issue_uid = collections.defaultdict(list)
625 changes_by_ref_issue_uid = collections.defaultdict(list)
626 changes_without_issue = []
627 for change in self.changes:
628 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000629 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100630 if issue_uid in issues:
631 changes_by_issue_uid[issue_uid].append(change)
632 added = True
633 if issue_uid in ref_issues:
634 changes_by_ref_issue_uid[issue_uid].append(change)
635 added = True
636 if not added:
637 changes_without_issue.append(change)
638
639 # Changes referencing own issues.
640 for issue_uid in issues:
641 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
642 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000643 if changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000644 print()
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000645 for change in changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000646 print(' ', end='') # this prints no newline
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000647 self.print_change(change)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000648 print()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100649
650 # Changes referencing others' issues.
651 for issue_uid in ref_issues:
652 assert changes_by_ref_issue_uid[issue_uid]
653 self.print_issue(ref_issues[issue_uid])
654 for change in changes_by_ref_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000655 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100656 self.print_change(change)
657
658 # Changes referencing no issues.
659 if changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000660 print(self.options.output_format_no_url.format(title='Other changes'))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100661 for change in changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000662 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100663 self.print_change(change)
664
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000665 def print_activity(self):
666 self.print_changes()
667 self.print_reviews()
668 self.print_issues()
669
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000670 def dump_json(self, ignore_keys=None):
671 if ignore_keys is None:
672 ignore_keys = ['replies']
673
674 def format_for_json_dump(in_array):
675 output = {}
676 for item in in_array:
677 url = item.get('url') or item.get('review_url')
678 if not url:
679 raise Exception('Dumped item %s does not specify url' % item)
680 output[url] = dict(
681 (k, v) for k,v in item.iteritems() if k not in ignore_keys)
682 return output
683
684 class PythonObjectEncoder(json.JSONEncoder):
685 def default(self, obj): # pylint: disable=method-hidden
686 if isinstance(obj, datetime):
687 return obj.isoformat()
688 if isinstance(obj, set):
689 return list(obj)
690 return json.JSONEncoder.default(self, obj)
691
692 output = {
693 'reviews': format_for_json_dump(self.reviews),
694 'changes': format_for_json_dump(self.changes),
695 'issues': format_for_json_dump(self.issues)
696 }
Raul Tambre80ee78e2019-05-06 22:41:05 +0000697 print(json.dumps(output, indent=2, cls=PythonObjectEncoder))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000698
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000699
700def main():
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000701 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
702 parser.add_option(
703 '-u', '--user', metavar='<email>',
Bruce Dawson84370962019-02-21 05:33:37 +0000704 # Look for USER and USERNAME (Windows) environment variables.
705 default=os.environ.get('USER', os.environ.get('USERNAME')),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000706 help='Filter on user, default=%default')
707 parser.add_option(
708 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000709 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000710 parser.add_option(
711 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000712 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000713 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
714 relativedelta(months=2))
715 parser.add_option(
716 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000717 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000718 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
719 parser.add_option(
720 '-Y', '--this_year', action='store_true',
721 help='Use this year\'s dates')
722 parser.add_option(
723 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000724 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000725 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000726 '-W', '--last_week', action='count',
727 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000728 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000729 '-a', '--auth',
730 action='store_true',
731 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000732 parser.add_option(
733 '-d', '--deltas',
734 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800735 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100736 parser.add_option(
737 '--no-referenced-issues',
738 action='store_true',
739 help='Do not fetch issues referenced by owned changes. Useful in '
740 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100741 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100742 parser.add_option(
743 '--skip-own-issues-without-changes',
744 action='store_true',
745 help='Skips listing own issues without changes when showing changes '
746 'grouped by referenced issue(s). See --changes-by-issue for more '
747 'details.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000748
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000749 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000750 'By default, all activity will be looked up and '
751 'printed. If any of these are specified, only '
752 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000753 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000754 '-c', '--changes',
755 action='store_true',
756 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000757 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000758 '-i', '--issues',
759 action='store_true',
760 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000761 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000762 '-r', '--reviews',
763 action='store_true',
764 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100765 activity_types_group.add_option(
766 '--changes-by-issue', action='store_true',
767 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000768 parser.add_option_group(activity_types_group)
769
770 output_format_group = optparse.OptionGroup(parser, 'Output Format',
771 'By default, all activity will be printed in the '
772 'following format: {url} {title}. This can be '
773 'changed for either all activity types or '
774 'individually for each activity type. The format '
775 'is defined as documented for '
776 'string.format(...). The variables available for '
Lutz Justen860378e2019-03-12 01:10:02 +0000777 'all activity types are url, title, author, '
778 'created and modified. Format options for '
779 'specific activity types will override the '
780 'generic format.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000781 output_format_group.add_option(
782 '-f', '--output-format', metavar='<format>',
783 default=u'{url} {title}',
784 help='Specifies the format to use when printing all your activity.')
785 output_format_group.add_option(
786 '--output-format-changes', metavar='<format>',
787 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000788 help='Specifies the format to use when printing changes. Supports the '
789 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000790 output_format_group.add_option(
791 '--output-format-issues', metavar='<format>',
792 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000793 help='Specifies the format to use when printing issues. Supports the '
794 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000795 output_format_group.add_option(
796 '--output-format-reviews', metavar='<format>',
797 default=None,
798 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000799 output_format_group.add_option(
800 '--output-format-heading', metavar='<format>',
801 default=u'{heading}:',
802 help='Specifies the format to use when printing headings.')
803 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100804 '--output-format-no-url', default='{title}',
805 help='Specifies the format to use when printing activity without url.')
806 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000807 '-m', '--markdown', action='store_true',
808 help='Use markdown-friendly output (overrides --output-format '
809 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000810 output_format_group.add_option(
811 '-j', '--json', action='store_true',
812 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000813 parser.add_option_group(output_format_group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000814 auth.add_auth_options(parser)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000815
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000816 parser.add_option(
817 '-v', '--verbose',
818 action='store_const',
819 dest='verbosity',
820 default=logging.WARN,
821 const=logging.INFO,
822 help='Output extra informational messages.'
823 )
824 parser.add_option(
825 '-q', '--quiet',
826 action='store_const',
827 dest='verbosity',
828 const=logging.ERROR,
829 help='Suppress non-error messages.'
830 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000831 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000832 '-M', '--merged-only',
833 action='store_true',
834 dest='merged_only',
835 default=False,
836 help='Shows only changes that have been merged.')
837 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000838 '-C', '--completed-issues',
839 action='store_true',
840 dest='completed_issues',
841 default=False,
842 help='Shows only monorail issues that have completed (Fixed|Verified) '
843 'by the user.')
844 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000845 '-o', '--output', metavar='<file>',
846 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000847
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000848 # Remove description formatting
849 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800850 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000851
852 options, args = parser.parse_args()
853 options.local_user = os.environ.get('USER')
854 if args:
855 parser.error('Args unsupported')
856 if not options.user:
Bruce Dawson84370962019-02-21 05:33:37 +0000857 parser.error('USER/USERNAME is not set, please use -u')
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000858 # Retains the original -u option as the email address.
859 options.email = options.user
860 options.user = username(options.email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000861
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000862 logging.basicConfig(level=options.verbosity)
863
864 # python-keyring provides easy access to the system keyring.
865 try:
866 import keyring # pylint: disable=unused-import,unused-variable,F0401
867 except ImportError:
868 logging.warning('Consider installing python-keyring')
869
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000870 if not options.begin:
871 if options.last_quarter:
872 begin, end = quarter_begin, quarter_end
873 elif options.this_year:
874 begin, end = get_year_of(datetime.today())
875 elif options.week_of:
876 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000877 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000878 begin, end = (get_week_of(datetime.today() -
879 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000880 else:
881 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
882 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700883 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000884 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700885 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000886 else:
887 end = datetime.today()
888 options.begin, options.end = begin, end
Bruce Dawson2afcf222019-02-25 22:07:08 +0000889 if begin >= end:
890 # The queries fail in peculiar ways when the begin date is in the future.
891 # Give a descriptive error message instead.
892 logging.error('Start date (%s) is the same or later than end date (%s)' %
893 (begin, end))
894 return 1
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000895
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000896 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000897 options.output_format_heading = '### {heading}\n'
898 options.output_format = ' * [{title}]({url})'
899 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000900 logging.info('Searching for activity by %s', options.user)
901 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000902
903 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100904 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000905
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100906 if not (options.changes or options.reviews or options.issues or
907 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000908 options.changes = True
909 options.issues = True
910 options.reviews = True
911
912 # First do any required authentication so none of the user interaction has to
913 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100914 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000915 my_activity.auth_for_changes()
916 if options.reviews:
917 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000918
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000919 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000920
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000921 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100922 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000923 my_activity.get_changes()
924 if options.reviews:
925 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100926 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000927 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100928 if not options.no_referenced_issues:
929 my_activity.get_referenced_issues()
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000930 except auth.AuthenticationError as e:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000931 logging.error('auth.AuthenticationError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000932
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100933 my_activity.show_progress('\n')
934
Vadim Bendebury8de38002018-05-14 19:02:55 -0700935 my_activity.print_access_errors()
936
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000937 output_file = None
938 try:
939 if options.output:
940 output_file = open(options.output, 'w')
941 logging.info('Printing output to "%s"', options.output)
942 sys.stdout = output_file
943 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700944 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000945 else:
946 if options.json:
947 my_activity.dump_json()
948 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100949 if options.changes:
950 my_activity.print_changes()
951 if options.reviews:
952 my_activity.print_reviews()
953 if options.issues:
954 my_activity.print_issues()
955 if options.changes_by_issue:
956 my_activity.print_changes_by_issue(
957 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000958 finally:
959 if output_file:
960 logging.info('Done printing to file.')
961 sys.stdout = sys.__stdout__
962 output_file.close()
963
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000964 return 0
965
966
967if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000968 # Fix encoding to support non-ascii issue titles.
969 fix_encoding.fix_encoding()
970
sbc@chromium.org013731e2015-02-26 18:28:43 +0000971 try:
972 sys.exit(main())
973 except KeyboardInterrupt:
974 sys.stderr.write('interrupted\n')
975 sys.exit(1)