blob: 2fc5db812fa14e6c05d78627edcf7dc8191c4dec [file] [log] [blame]
Edward Lemura3b6fd02020-03-02 22:16:15 +00001#!/usr/bin/env vpython3
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
Raul Tambre80ee78e2019-05-06 22:41:05 +000024from __future__ import print_function
25
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010026import collections
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +010027import contextlib
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000028from datetime import datetime
29from datetime import timedelta
Edward Lemur202c5592019-10-21 22:44:52 +000030import httplib2
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010031import itertools
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000032import json
Tobias Sargeantffb3c432017-03-08 14:09:14 +000033import logging
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +010034from multiprocessing.pool import ThreadPool
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000035import optparse
36import os
37import subprocess
Tobias Sargeantffb3c432017-03-08 14:09:14 +000038from string import Formatter
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000039import sys
40import urllib
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +000041import re
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000042
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
stevefung@chromium.org832d51e2015-05-27 12:52:51 +000044import fix_encoding
Edward Lesmesae3586b2020-03-23 21:21:14 +000045import gclient_utils
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000046import gerrit_util
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000047
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +000048
Edward Lemur2a048032020-01-14 22:58:13 +000049if sys.version_info.major == 2:
Edward Lemura3b6fd02020-03-02 22:16:15 +000050 logging.warning(
51 'Python 2 is deprecated. Run my_activity.py using vpython3.')
Edward Lemur2a048032020-01-14 22:58:13 +000052 import urllib as urllib_parse
53else:
54 import urllib.parse as urllib_parse
55
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000056try:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000057 import dateutil # pylint: disable=import-error
58 import dateutil.parser
59 from dateutil.relativedelta import relativedelta
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000060except ImportError:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000061 logging.error('python-dateutil package required')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000062 exit(1)
63
Tobias Sargeantffb3c432017-03-08 14:09:14 +000064
65class DefaultFormatter(Formatter):
66 def __init__(self, default = ''):
67 super(DefaultFormatter, self).__init__()
68 self.default = default
69
70 def get_value(self, key, args, kwds):
Edward Lemur2a048032020-01-14 22:58:13 +000071 if isinstance(key, str) and key not in kwds:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000072 return self.default
73 return Formatter.get_value(self, key, args, kwds)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000074
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000075gerrit_instances = [
76 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000077 'url': 'android-review.googlesource.com',
Mike Frysingerfdf027a2020-02-27 21:53:45 +000078 'shorturl': 'r.android.com',
79 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000080 },
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000081 {
Mike Frysinger24fd8632020-02-27 22:07:06 +000082 'url': 'gerrit-review.googlesource.com',
83 },
84 {
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000085 'url': 'chrome-internal-review.googlesource.com',
Jeremy Romanf475a472017-06-06 16:49:11 -040086 'shorturl': 'crrev.com/i',
Varun Khanejad9f97bc2017-08-02 12:55:01 -070087 'short_url_protocol': 'https',
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000088 },
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000089 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000090 'url': 'chromium-review.googlesource.com',
91 'shorturl': 'crrev.com/c',
92 'short_url_protocol': 'https',
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000093 },
Ryan Harrison897602a2017-09-18 16:23:41 -040094 {
Ryan Harrison06e18692019-09-23 18:22:25 +000095 'url': 'dawn-review.googlesource.com',
96 },
97 {
Ryan Harrison897602a2017-09-18 16:23:41 -040098 'url': 'pdfium-review.googlesource.com',
99 },
Adrienne Walker95d4c852018-09-27 20:28:12 +0000100 {
101 'url': 'skia-review.googlesource.com',
102 },
Paul Fagerburgb93d82c2020-08-17 16:19:46 +0000103 {
104 'url': 'review.coreboot.org',
105 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000106]
107
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100108monorail_projects = {
Jamie Madill1db68ea2019-09-03 19:34:42 +0000109 'angleproject': {
110 'shorturl': 'anglebug.com',
111 'short_url_protocol': 'http',
112 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100113 'chromium': {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000114 'shorturl': 'crbug.com',
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700115 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000116 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000117 'dawn': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100118 'google-breakpad': {},
119 'gyp': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100120 'pdfium': {
Ryan Harrison897602a2017-09-18 16:23:41 -0400121 'shorturl': 'crbug.com/pdfium',
122 'short_url_protocol': 'https',
123 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000124 'skia': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100125 'v8': {
126 'shorturl': 'crbug.com/v8',
127 'short_url_protocol': 'https',
128 },
129}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000130
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000131def username(email):
132 """Keeps the username of an email address."""
133 return email and email.split('@', 1)[0]
134
135
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000136def datetime_to_midnight(date):
137 return date - timedelta(hours=date.hour, minutes=date.minute,
138 seconds=date.second, microseconds=date.microsecond)
139
140
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000141def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000142 begin = (datetime_to_midnight(date) -
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000143 relativedelta(months=(date.month - 1) % 3, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000144 return begin, begin + relativedelta(months=3)
145
146
147def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000148 begin = (datetime_to_midnight(date) -
149 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000150 return begin, begin + relativedelta(years=1)
151
152
153def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000154 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000155 return begin, begin + timedelta(days=7)
156
157
158def get_yes_or_no(msg):
159 while True:
Edward Lesmesae3586b2020-03-23 21:21:14 +0000160 response = gclient_utils.AskForData(msg + ' yes/no [no] ')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000161 if response == 'y' or response == 'yes':
162 return True
163 elif not response or response == 'n' or response == 'no':
164 return False
165
166
deymo@chromium.org6c039202013-09-12 12:28:12 +0000167def datetime_from_gerrit(date_string):
168 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
169
170
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100171def datetime_from_monorail(date_string):
172 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000173
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000174def extract_bug_numbers_from_description(issue):
175 # Getting the description for REST Gerrit
176 revision = issue['revisions'][issue['current_revision']]
177 description = revision['commit']['message']
178
179 bugs = []
180 # Handle both "Bug: 99999" and "BUG=99999" bug notations
181 # Multiple bugs can be noted on a single line or in multiple ones.
182 matches = re.findall(
183 r'^(BUG=|(Bug|Fixed):\s*)((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)',
184 description, flags=re.IGNORECASE | re.MULTILINE)
185 if matches:
186 for match in matches:
187 bugs.extend(match[2].replace(' ', '').split(','))
188 # Add default chromium: prefix if none specified.
189 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
190
191 return sorted(set(bugs))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000192
193class MyActivity(object):
194 def __init__(self, options):
195 self.options = options
196 self.modified_after = options.begin
197 self.modified_before = options.end
198 self.user = options.user
199 self.changes = []
200 self.reviews = []
201 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100202 self.referenced_issues = []
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000203 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-14 19:02:55 -0700204 self.access_errors = set()
Vadim Bendebury80012972019-11-23 02:23:11 +0000205 self.skip_servers = (options.skip_servers.split(','))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000206
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100207 def show_progress(self, how='.'):
208 if sys.stdout.isatty():
209 sys.stdout.write(how)
210 sys.stdout.flush()
211
Vadim Bendebury8de38002018-05-14 19:02:55 -0700212 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200213 # Convert the "key:value" filter to a list of (key, value) pairs.
214 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000215 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000216 # Instantiate the generator to force all the requests now and catch the
217 # errors here.
218 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000219 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
220 'CURRENT_REVISION', 'CURRENT_COMMIT']))
Raul Tambre7c938462019-05-24 16:35:35 +0000221 except gerrit_util.GerritError as e:
Vadim Bendebury8de38002018-05-14 19:02:55 -0700222 error_message = 'Looking up %r: %s' % (instance['url'], e)
223 if error_message not in self.access_errors:
224 self.access_errors.add(error_message)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000225 return []
226
deymo@chromium.org6c039202013-09-12 12:28:12 +0000227 def gerrit_search(self, instance, owner=None, reviewer=None):
Vadim Bendebury80012972019-11-23 02:23:11 +0000228 if instance['url'] in self.skip_servers:
229 return []
deymo@chromium.org6c039202013-09-12 12:28:12 +0000230 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'])
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000278 ret['bugs'] = extract_bug_numbers_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000279 return ret
280
281 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700282 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000283 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000284 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
285 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000286 for reply in replies:
287 ret.append({
288 'author': reply['author']['email'],
289 'created': datetime_from_gerrit(reply['date']),
290 'content': reply['message'],
291 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000292 return ret
293
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100294 def monorail_get_auth_http(self):
Kenneth Russell66badbd2018-09-09 21:35:32 +0000295 # Manually use a long timeout (10m); for some users who have a
296 # long history on the issue tracker, whatever the default timeout
297 # is is reached.
Edward Lemur5b929a42019-10-21 17:57:39 +0000298 return auth.Authenticator().authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100299
300 def filter_modified_monorail_issue(self, issue):
301 """Precisely checks if an issue has been modified in the time range.
302
303 This fetches all issue comments to check if the issue has been modified in
304 the time range specified by user. This is needed because monorail only
305 allows filtering by last updated and published dates, which is not
306 sufficient to tell whether a given issue has been modified at some specific
307 time range. Any update to the issue is a reported as comment on Monorail.
308
309 Args:
310 issue: Issue dict as returned by monorail_query_issues method. In
311 particular, must have a key 'uid' formatted as 'project:issue_id'.
312
313 Returns:
314 Passed issue if modified, None otherwise.
315 """
316 http = self.monorail_get_auth_http()
317 project, issue_id = issue['uid'].split(':')
318 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
319 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
320 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100321 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100322 content = json.loads(body)
323 if not content:
324 logging.error('Unable to parse %s response from monorail.', project)
325 return issue
326
327 for item in content.get('items', []):
328 comment_published = datetime_from_monorail(item['published'])
329 if self.filter_modified(comment_published):
330 return issue
331
332 return None
333
334 def monorail_query_issues(self, project, query):
335 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000336 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100337 '/%s/issues') % project
Edward Lemur2a048032020-01-14 22:58:13 +0000338 query_data = urllib_parse.urlencode(query)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100339 url = url + '?' + query_data
340 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100341 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100342 content = json.loads(body)
343 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100344 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100345 return []
346
347 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100348 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100349 for item in content.get('items', []):
350 if project_config.get('shorturl'):
351 protocol = project_config.get('short_url_protocol', 'http')
352 item_url = '%s://%s/%d' % (
353 protocol, project_config['shorturl'], item['id'])
354 else:
355 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
356 project, item['id'])
357 issue = {
358 'uid': '%s:%s' % (project, item['id']),
359 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100360 'created': datetime_from_monorail(item['published']),
361 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100362 'author': item['author']['name'],
363 'url': item_url,
364 'comments': [],
365 'status': item['status'],
366 'labels': [],
367 'components': []
368 }
369 if 'owner' in item:
370 issue['owner'] = item['owner']['name']
371 else:
372 issue['owner'] = 'None'
373 if 'labels' in item:
374 issue['labels'] = item['labels']
375 if 'components' in item:
376 issue['components'] = item['components']
377 issues.append(issue)
378
379 return issues
380
381 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000382 epoch = datetime.utcfromtimestamp(0)
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000383 # Defaults to @chromium.org email if one wasn't provided on -u option.
384 user_str = (self.options.email if self.options.email.find('@') >= 0
385 else '%s@chromium.org' % self.user)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000386
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100387 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000388 'maxResults': 10000,
389 'q': user_str,
390 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
391 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000392 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000393
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000394 if self.options.completed_issues:
395 return [
396 issue for issue in issues
397 if (self.match(issue['owner']) and
398 issue['status'].lower() in ('verified', 'fixed'))
399 ]
400
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100401 return [
402 issue for issue in issues
403 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000404
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100405 def monorail_get_issues(self, project, issue_ids):
406 return self.monorail_query_issues(project, {
407 'maxResults': 10000,
408 'q': 'id:%s' % ','.join(issue_ids)
409 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000410
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000411 def print_heading(self, heading):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000412 print()
413 print(self.options.output_format_heading.format(heading=heading))
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000414
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000415 def match(self, author):
416 if '@' in self.user:
417 return author == self.user
418 return author.startswith(self.user + '@')
419
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000420 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000421 activity = len([
422 reply
423 for reply in change['replies']
424 if self.match(reply['author'])
425 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000426 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000427 'created': change['created'].date().isoformat(),
428 'modified': change['modified'].date().isoformat(),
429 'reviewers': ', '.join(change['reviewers']),
430 'status': change['status'],
431 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000432 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000433 if self.options.deltas:
434 optional_values['delta'] = change['delta']
435
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000436 self.print_generic(self.options.output_format,
437 self.options.output_format_changes,
438 change['header'],
439 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000440 change['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000441 change['created'],
442 change['modified'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000443 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000444
445 def print_issue(self, issue):
446 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000447 'created': issue['created'].date().isoformat(),
448 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000449 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000450 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000451 }
452 self.print_generic(self.options.output_format,
453 self.options.output_format_issues,
454 issue['header'],
455 issue['url'],
456 issue['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000457 issue['created'],
458 issue['modified'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000459 optional_values)
460
461 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000462 activity = len([
463 reply
464 for reply in review['replies']
465 if self.match(reply['author'])
466 ])
467 optional_values = {
468 'created': review['created'].date().isoformat(),
469 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800470 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000471 'activity': activity,
472 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800473 if self.options.deltas:
474 optional_values['delta'] = review['delta']
475
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000476 self.print_generic(self.options.output_format,
477 self.options.output_format_reviews,
478 review['header'],
479 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000480 review['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000481 review['created'],
482 review['modified'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000483 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000484
485 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000486 def print_generic(default_fmt, specific_fmt,
Lutz Justen860378e2019-03-12 01:10:02 +0000487 title, url, author, created, modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000488 optional_values=None):
489 output_format = specific_fmt if specific_fmt is not None else default_fmt
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000490 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000491 'title': title,
492 'url': url,
493 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000494 'created': created,
495 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000496 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000497 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000498 values.update(optional_values)
Edward Lemur2a048032020-01-14 22:58:13 +0000499 print(DefaultFormatter().format(output_format, **values))
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000500
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000501
502 def filter_issue(self, issue, should_filter_by_user=True):
503 def maybe_filter_username(email):
504 return not should_filter_by_user or username(email) == self.user
505 if (maybe_filter_username(issue['author']) and
506 self.filter_modified(issue['created'])):
507 return True
508 if (maybe_filter_username(issue['owner']) and
509 (self.filter_modified(issue['created']) or
510 self.filter_modified(issue['modified']))):
511 return True
512 for reply in issue['replies']:
513 if self.filter_modified(reply['created']):
514 if not should_filter_by_user:
515 break
516 if (username(reply['author']) == self.user
517 or (self.user + '@') in reply['content']):
518 break
519 else:
520 return False
521 return True
522
523 def filter_modified(self, modified):
524 return self.modified_after < modified and modified < self.modified_before
525
526 def auth_for_changes(self):
527 #TODO(cjhopman): Move authentication check for getting changes here.
528 pass
529
530 def auth_for_reviews(self):
531 # Reviews use all the same instances as changes so no authentication is
532 # required.
533 pass
534
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000535 def get_changes(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000536 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100537 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100538 gerrit_changes = pool.map_async(
539 lambda instance: self.gerrit_search(instance, owner=self.user),
540 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100541 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000542 self.changes = list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000543
544 def print_changes(self):
545 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000546 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000547 for change in self.changes:
Bruce Dawson2afcf222019-02-25 22:07:08 +0000548 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000549
Vadim Bendebury8de38002018-05-14 19:02:55 -0700550 def print_access_errors(self):
551 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400552 logging.error('Access Errors:')
553 for error in self.access_errors:
554 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700555
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000556 def get_reviews(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000557 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100558 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100559 gerrit_reviews = pool.map_async(
560 lambda instance: self.gerrit_search(instance, reviewer=self.user),
561 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100562 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000563 self.reviews = list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000564
565 def print_reviews(self):
566 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000567 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000568 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000569 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000570
571 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100572 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
573 monorail_issues = pool.map(
574 self.monorail_issue_search, monorail_projects.keys())
575 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
576
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700577 if not monorail_issues:
578 return
579
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100580 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
581 filtered_issues = pool.map(
582 self.filter_modified_monorail_issue, monorail_issues)
583 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100584
585 def get_referenced_issues(self):
586 if not self.issues:
587 self.get_issues()
588
589 if not self.changes:
590 self.get_changes()
591
592 referenced_issue_uids = set(itertools.chain.from_iterable(
593 change['bugs'] for change in self.changes))
594 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
595 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
596
597 missing_issues_by_project = collections.defaultdict(list)
598 for issue_uid in missing_issue_uids:
599 project, issue_id = issue_uid.split(':')
600 missing_issues_by_project[project].append(issue_id)
601
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000602 for project, issue_ids in missing_issues_by_project.items():
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100603 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000604
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000605 def print_issues(self):
606 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000607 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000608 for issue in self.issues:
609 self.print_issue(issue)
610
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100611 def print_changes_by_issue(self, skip_empty_own):
612 if not self.issues or not self.changes:
613 return
614
615 self.print_heading('Changes by referenced issue(s)')
616 issues = {issue['uid']: issue for issue in self.issues}
617 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
618 changes_by_issue_uid = collections.defaultdict(list)
619 changes_by_ref_issue_uid = collections.defaultdict(list)
620 changes_without_issue = []
621 for change in self.changes:
622 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000623 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100624 if issue_uid in issues:
625 changes_by_issue_uid[issue_uid].append(change)
626 added = True
627 if issue_uid in ref_issues:
628 changes_by_ref_issue_uid[issue_uid].append(change)
629 added = True
630 if not added:
631 changes_without_issue.append(change)
632
633 # Changes referencing own issues.
634 for issue_uid in issues:
635 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
636 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000637 if changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000638 print()
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000639 for change in changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000640 print(' ', end='') # this prints no newline
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000641 self.print_change(change)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000642 print()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100643
644 # Changes referencing others' issues.
645 for issue_uid in ref_issues:
646 assert changes_by_ref_issue_uid[issue_uid]
647 self.print_issue(ref_issues[issue_uid])
648 for change in changes_by_ref_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000649 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100650 self.print_change(change)
651
652 # Changes referencing no issues.
653 if changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000654 print(self.options.output_format_no_url.format(title='Other changes'))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100655 for change in changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000656 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100657 self.print_change(change)
658
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000659 def print_activity(self):
660 self.print_changes()
661 self.print_reviews()
662 self.print_issues()
663
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000664 def dump_json(self, ignore_keys=None):
665 if ignore_keys is None:
666 ignore_keys = ['replies']
667
668 def format_for_json_dump(in_array):
669 output = {}
670 for item in in_array:
671 url = item.get('url') or item.get('review_url')
672 if not url:
673 raise Exception('Dumped item %s does not specify url' % item)
674 output[url] = dict(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000675 (k, v) for k,v in item.items() if k not in ignore_keys)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000676 return output
677
678 class PythonObjectEncoder(json.JSONEncoder):
679 def default(self, obj): # pylint: disable=method-hidden
680 if isinstance(obj, datetime):
681 return obj.isoformat()
682 if isinstance(obj, set):
683 return list(obj)
684 return json.JSONEncoder.default(self, obj)
685
686 output = {
687 'reviews': format_for_json_dump(self.reviews),
688 'changes': format_for_json_dump(self.changes),
689 'issues': format_for_json_dump(self.issues)
690 }
Raul Tambre80ee78e2019-05-06 22:41:05 +0000691 print(json.dumps(output, indent=2, cls=PythonObjectEncoder))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000692
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000693
694def main():
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000695 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
696 parser.add_option(
697 '-u', '--user', metavar='<email>',
Bruce Dawson84370962019-02-21 05:33:37 +0000698 # Look for USER and USERNAME (Windows) environment variables.
699 default=os.environ.get('USER', os.environ.get('USERNAME')),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000700 help='Filter on user, default=%default')
701 parser.add_option(
702 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000703 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000704 parser.add_option(
705 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000706 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000707 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
708 relativedelta(months=2))
709 parser.add_option(
710 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000711 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000712 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
713 parser.add_option(
714 '-Y', '--this_year', action='store_true',
715 help='Use this year\'s dates')
716 parser.add_option(
717 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000718 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000719 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000720 '-W', '--last_week', action='count',
721 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000722 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000723 '-a', '--auth',
724 action='store_true',
725 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000726 parser.add_option(
727 '-d', '--deltas',
728 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800729 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100730 parser.add_option(
731 '--no-referenced-issues',
732 action='store_true',
733 help='Do not fetch issues referenced by owned changes. Useful in '
734 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100735 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100736 parser.add_option(
Vadim Bendebury80012972019-11-23 02:23:11 +0000737 '--skip_servers',
738 action='store',
739 default='',
740 help='A comma separated list of gerrit and rietveld servers to ignore')
741 parser.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100742 '--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}:',
Vincent Scheib2be61a12020-05-26 16:45:32 +0000801 help='Specifies the format to use when printing headings. '
802 'Supports the variable {heading}.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000803 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)
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()
Edward Lemur5b929a42019-10-21 17:57:39 +0000929 except auth.LoginRequiredError as e:
930 logging.error('auth.LoginRequiredError: %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)