blob: 375f77585dea173fbc3fd5b0af2607301b88ed29 [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': {},
Ryan Harrison97811152021-03-29 20:30:57 +0000125 'tint': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100126 'v8': {
127 'shorturl': 'crbug.com/v8',
128 'short_url_protocol': 'https',
129 },
130}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000131
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000132def username(email):
133 """Keeps the username of an email address."""
134 return email and email.split('@', 1)[0]
135
136
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000137def datetime_to_midnight(date):
138 return date - timedelta(hours=date.hour, minutes=date.minute,
139 seconds=date.second, microseconds=date.microsecond)
140
141
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000142def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000143 begin = (datetime_to_midnight(date) -
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000144 relativedelta(months=(date.month - 1) % 3, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000145 return begin, begin + relativedelta(months=3)
146
147
148def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000149 begin = (datetime_to_midnight(date) -
150 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000151 return begin, begin + relativedelta(years=1)
152
153
154def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000155 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000156 return begin, begin + timedelta(days=7)
157
158
159def get_yes_or_no(msg):
160 while True:
Edward Lesmesae3586b2020-03-23 21:21:14 +0000161 response = gclient_utils.AskForData(msg + ' yes/no [no] ')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000162 if response == 'y' or response == 'yes':
163 return True
164 elif not response or response == 'n' or response == 'no':
165 return False
166
167
deymo@chromium.org6c039202013-09-12 12:28:12 +0000168def datetime_from_gerrit(date_string):
169 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
170
171
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100172def datetime_from_monorail(date_string):
173 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000174
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000175def extract_bug_numbers_from_description(issue):
176 # Getting the description for REST Gerrit
177 revision = issue['revisions'][issue['current_revision']]
178 description = revision['commit']['message']
179
180 bugs = []
181 # Handle both "Bug: 99999" and "BUG=99999" bug notations
182 # Multiple bugs can be noted on a single line or in multiple ones.
183 matches = re.findall(
184 r'^(BUG=|(Bug|Fixed):\s*)((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)',
185 description, flags=re.IGNORECASE | re.MULTILINE)
186 if matches:
187 for match in matches:
188 bugs.extend(match[2].replace(' ', '').split(','))
189 # Add default chromium: prefix if none specified.
190 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
191
192 return sorted(set(bugs))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000193
194class MyActivity(object):
195 def __init__(self, options):
196 self.options = options
197 self.modified_after = options.begin
198 self.modified_before = options.end
199 self.user = options.user
200 self.changes = []
201 self.reviews = []
202 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100203 self.referenced_issues = []
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000204 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-14 19:02:55 -0700205 self.access_errors = set()
Vadim Bendebury80012972019-11-23 02:23:11 +0000206 self.skip_servers = (options.skip_servers.split(','))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000207
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100208 def show_progress(self, how='.'):
209 if sys.stdout.isatty():
210 sys.stdout.write(how)
211 sys.stdout.flush()
212
Vadim Bendebury8de38002018-05-14 19:02:55 -0700213 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200214 # Convert the "key:value" filter to a list of (key, value) pairs.
215 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000216 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000217 # Instantiate the generator to force all the requests now and catch the
218 # errors here.
219 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000220 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
221 'CURRENT_REVISION', 'CURRENT_COMMIT']))
Raul Tambre7c938462019-05-24 16:35:35 +0000222 except gerrit_util.GerritError as e:
Vadim Bendebury8de38002018-05-14 19:02:55 -0700223 error_message = 'Looking up %r: %s' % (instance['url'], e)
224 if error_message not in self.access_errors:
225 self.access_errors.add(error_message)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000226 return []
227
deymo@chromium.org6c039202013-09-12 12:28:12 +0000228 def gerrit_search(self, instance, owner=None, reviewer=None):
Vadim Bendebury80012972019-11-23 02:23:11 +0000229 if instance['url'] in self.skip_servers:
230 return []
deymo@chromium.org6c039202013-09-12 12:28:12 +0000231 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'])
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000279 ret['bugs'] = 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):
Kenneth Russell66badbd2018-09-09 21:35:32 +0000296 # Manually use a long timeout (10m); for some users who have a
297 # long history on the issue tracker, whatever the default timeout
298 # is is reached.
Edward Lemur5b929a42019-10-21 17:57:39 +0000299 return auth.Authenticator().authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100300
301 def filter_modified_monorail_issue(self, issue):
302 """Precisely checks if an issue has been modified in the time range.
303
304 This fetches all issue comments to check if the issue has been modified in
305 the time range specified by user. This is needed because monorail only
306 allows filtering by last updated and published dates, which is not
307 sufficient to tell whether a given issue has been modified at some specific
308 time range. Any update to the issue is a reported as comment on Monorail.
309
310 Args:
311 issue: Issue dict as returned by monorail_query_issues method. In
312 particular, must have a key 'uid' formatted as 'project:issue_id'.
313
314 Returns:
315 Passed issue if modified, None otherwise.
316 """
317 http = self.monorail_get_auth_http()
318 project, issue_id = issue['uid'].split(':')
319 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
320 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
321 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100322 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100323 content = json.loads(body)
324 if not content:
325 logging.error('Unable to parse %s response from monorail.', project)
326 return issue
327
328 for item in content.get('items', []):
329 comment_published = datetime_from_monorail(item['published'])
330 if self.filter_modified(comment_published):
331 return issue
332
333 return None
334
335 def monorail_query_issues(self, project, query):
336 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000337 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100338 '/%s/issues') % project
Edward Lemur2a048032020-01-14 22:58:13 +0000339 query_data = urllib_parse.urlencode(query)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100340 url = url + '?' + query_data
341 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100342 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100343 content = json.loads(body)
344 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100345 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100346 return []
347
348 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100349 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100350 for item in content.get('items', []):
351 if project_config.get('shorturl'):
352 protocol = project_config.get('short_url_protocol', 'http')
353 item_url = '%s://%s/%d' % (
354 protocol, project_config['shorturl'], item['id'])
355 else:
356 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
357 project, item['id'])
358 issue = {
359 'uid': '%s:%s' % (project, item['id']),
360 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100361 'created': datetime_from_monorail(item['published']),
362 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100363 'author': item['author']['name'],
364 'url': item_url,
365 'comments': [],
366 'status': item['status'],
367 'labels': [],
368 'components': []
369 }
370 if 'owner' in item:
371 issue['owner'] = item['owner']['name']
372 else:
373 issue['owner'] = 'None'
374 if 'labels' in item:
375 issue['labels'] = item['labels']
376 if 'components' in item:
377 issue['components'] = item['components']
378 issues.append(issue)
379
380 return issues
381
382 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000383 epoch = datetime.utcfromtimestamp(0)
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000384 # Defaults to @chromium.org email if one wasn't provided on -u option.
385 user_str = (self.options.email if self.options.email.find('@') >= 0
386 else '%s@chromium.org' % self.user)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000387
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100388 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000389 'maxResults': 10000,
390 'q': user_str,
391 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
392 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000393 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000394
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000395 if self.options.completed_issues:
396 return [
397 issue for issue in issues
398 if (self.match(issue['owner']) and
399 issue['status'].lower() in ('verified', 'fixed'))
400 ]
401
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100402 return [
403 issue for issue in issues
404 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000405
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100406 def monorail_get_issues(self, project, issue_ids):
407 return self.monorail_query_issues(project, {
408 'maxResults': 10000,
409 'q': 'id:%s' % ','.join(issue_ids)
410 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000411
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000412 def print_heading(self, heading):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000413 print()
414 print(self.options.output_format_heading.format(heading=heading))
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000415
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000416 def match(self, author):
417 if '@' in self.user:
418 return author == self.user
419 return author.startswith(self.user + '@')
420
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000421 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000422 activity = len([
423 reply
424 for reply in change['replies']
425 if self.match(reply['author'])
426 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000427 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000428 'created': change['created'].date().isoformat(),
429 'modified': change['modified'].date().isoformat(),
430 'reviewers': ', '.join(change['reviewers']),
431 'status': change['status'],
432 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000433 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000434 if self.options.deltas:
435 optional_values['delta'] = change['delta']
436
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000437 self.print_generic(self.options.output_format,
438 self.options.output_format_changes,
439 change['header'],
440 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000441 change['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000442 change['created'],
443 change['modified'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000444 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000445
446 def print_issue(self, issue):
447 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000448 'created': issue['created'].date().isoformat(),
449 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000450 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000451 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000452 }
453 self.print_generic(self.options.output_format,
454 self.options.output_format_issues,
455 issue['header'],
456 issue['url'],
457 issue['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000458 issue['created'],
459 issue['modified'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000460 optional_values)
461
462 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000463 activity = len([
464 reply
465 for reply in review['replies']
466 if self.match(reply['author'])
467 ])
468 optional_values = {
469 'created': review['created'].date().isoformat(),
470 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800471 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000472 'activity': activity,
473 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800474 if self.options.deltas:
475 optional_values['delta'] = review['delta']
476
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000477 self.print_generic(self.options.output_format,
478 self.options.output_format_reviews,
479 review['header'],
480 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000481 review['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000482 review['created'],
483 review['modified'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000484 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000485
486 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000487 def print_generic(default_fmt, specific_fmt,
Lutz Justen860378e2019-03-12 01:10:02 +0000488 title, url, author, created, modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000489 optional_values=None):
490 output_format = specific_fmt if specific_fmt is not None else default_fmt
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000491 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000492 'title': title,
493 'url': url,
494 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000495 'created': created,
496 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000497 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000498 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000499 values.update(optional_values)
Edward Lemur2a048032020-01-14 22:58:13 +0000500 print(DefaultFormatter().format(output_format, **values))
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000501
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000502
503 def filter_issue(self, issue, should_filter_by_user=True):
504 def maybe_filter_username(email):
505 return not should_filter_by_user or username(email) == self.user
506 if (maybe_filter_username(issue['author']) and
507 self.filter_modified(issue['created'])):
508 return True
509 if (maybe_filter_username(issue['owner']) and
510 (self.filter_modified(issue['created']) or
511 self.filter_modified(issue['modified']))):
512 return True
513 for reply in issue['replies']:
514 if self.filter_modified(reply['created']):
515 if not should_filter_by_user:
516 break
517 if (username(reply['author']) == self.user
518 or (self.user + '@') in reply['content']):
519 break
520 else:
521 return False
522 return True
523
524 def filter_modified(self, modified):
525 return self.modified_after < modified and modified < self.modified_before
526
527 def auth_for_changes(self):
528 #TODO(cjhopman): Move authentication check for getting changes here.
529 pass
530
531 def auth_for_reviews(self):
532 # Reviews use all the same instances as changes so no authentication is
533 # required.
534 pass
535
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000536 def get_changes(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000537 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100538 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100539 gerrit_changes = pool.map_async(
540 lambda instance: self.gerrit_search(instance, owner=self.user),
541 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100542 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000543 self.changes = list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000544
545 def print_changes(self):
546 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000547 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000548 for change in self.changes:
Bruce Dawson2afcf222019-02-25 22:07:08 +0000549 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000550
Vadim Bendebury8de38002018-05-14 19:02:55 -0700551 def print_access_errors(self):
552 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400553 logging.error('Access Errors:')
554 for error in self.access_errors:
555 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700556
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000557 def get_reviews(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000558 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100559 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100560 gerrit_reviews = pool.map_async(
561 lambda instance: self.gerrit_search(instance, reviewer=self.user),
562 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100563 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000564 self.reviews = list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000565
566 def print_reviews(self):
567 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000568 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000569 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000570 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000571
572 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100573 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
574 monorail_issues = pool.map(
575 self.monorail_issue_search, monorail_projects.keys())
576 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
577
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700578 if not monorail_issues:
579 return
580
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100581 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
582 filtered_issues = pool.map(
583 self.filter_modified_monorail_issue, monorail_issues)
584 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100585
586 def get_referenced_issues(self):
587 if not self.issues:
588 self.get_issues()
589
590 if not self.changes:
591 self.get_changes()
592
593 referenced_issue_uids = set(itertools.chain.from_iterable(
594 change['bugs'] for change in self.changes))
595 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
596 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
597
598 missing_issues_by_project = collections.defaultdict(list)
599 for issue_uid in missing_issue_uids:
600 project, issue_id = issue_uid.split(':')
601 missing_issues_by_project[project].append(issue_id)
602
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000603 for project, issue_ids in missing_issues_by_project.items():
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100604 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000605
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000606 def print_issues(self):
607 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000608 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000609 for issue in self.issues:
610 self.print_issue(issue)
611
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100612 def print_changes_by_issue(self, skip_empty_own):
613 if not self.issues or not self.changes:
614 return
615
616 self.print_heading('Changes by referenced issue(s)')
617 issues = {issue['uid']: issue for issue in self.issues}
618 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
619 changes_by_issue_uid = collections.defaultdict(list)
620 changes_by_ref_issue_uid = collections.defaultdict(list)
621 changes_without_issue = []
622 for change in self.changes:
623 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000624 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100625 if issue_uid in issues:
626 changes_by_issue_uid[issue_uid].append(change)
627 added = True
628 if issue_uid in ref_issues:
629 changes_by_ref_issue_uid[issue_uid].append(change)
630 added = True
631 if not added:
632 changes_without_issue.append(change)
633
634 # Changes referencing own issues.
635 for issue_uid in issues:
636 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
637 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000638 if changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000639 print()
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000640 for change in changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000641 print(' ', end='') # this prints no newline
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000642 self.print_change(change)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000643 print()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100644
645 # Changes referencing others' issues.
646 for issue_uid in ref_issues:
647 assert changes_by_ref_issue_uid[issue_uid]
648 self.print_issue(ref_issues[issue_uid])
649 for change in changes_by_ref_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000650 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100651 self.print_change(change)
652
653 # Changes referencing no issues.
654 if changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000655 print(self.options.output_format_no_url.format(title='Other changes'))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100656 for change in changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000657 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100658 self.print_change(change)
659
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000660 def print_activity(self):
661 self.print_changes()
662 self.print_reviews()
663 self.print_issues()
664
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000665 def dump_json(self, ignore_keys=None):
666 if ignore_keys is None:
667 ignore_keys = ['replies']
668
669 def format_for_json_dump(in_array):
670 output = {}
671 for item in in_array:
672 url = item.get('url') or item.get('review_url')
673 if not url:
674 raise Exception('Dumped item %s does not specify url' % item)
675 output[url] = dict(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000676 (k, v) for k,v in item.items() if k not in ignore_keys)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000677 return output
678
679 class PythonObjectEncoder(json.JSONEncoder):
680 def default(self, obj): # pylint: disable=method-hidden
681 if isinstance(obj, datetime):
682 return obj.isoformat()
683 if isinstance(obj, set):
684 return list(obj)
685 return json.JSONEncoder.default(self, obj)
686
687 output = {
688 'reviews': format_for_json_dump(self.reviews),
689 'changes': format_for_json_dump(self.changes),
690 'issues': format_for_json_dump(self.issues)
691 }
Raul Tambre80ee78e2019-05-06 22:41:05 +0000692 print(json.dumps(output, indent=2, cls=PythonObjectEncoder))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000693
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000694
695def main():
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000696 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
697 parser.add_option(
698 '-u', '--user', metavar='<email>',
Bruce Dawson84370962019-02-21 05:33:37 +0000699 # Look for USER and USERNAME (Windows) environment variables.
700 default=os.environ.get('USER', os.environ.get('USERNAME')),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000701 help='Filter on user, default=%default')
702 parser.add_option(
703 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000704 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000705 parser.add_option(
706 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000707 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000708 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
709 relativedelta(months=2))
710 parser.add_option(
711 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000712 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000713 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
714 parser.add_option(
715 '-Y', '--this_year', action='store_true',
716 help='Use this year\'s dates')
717 parser.add_option(
718 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000719 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000720 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000721 '-W', '--last_week', action='count',
722 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000723 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000724 '-a', '--auth',
725 action='store_true',
726 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000727 parser.add_option(
728 '-d', '--deltas',
729 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800730 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100731 parser.add_option(
732 '--no-referenced-issues',
733 action='store_true',
734 help='Do not fetch issues referenced by owned changes. Useful in '
735 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100736 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100737 parser.add_option(
Vadim Bendebury80012972019-11-23 02:23:11 +0000738 '--skip_servers',
739 action='store',
740 default='',
741 help='A comma separated list of gerrit and rietveld servers to ignore')
742 parser.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100743 '--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}:',
Vincent Scheib2be61a12020-05-26 16:45:32 +0000802 help='Specifies the format to use when printing headings. '
803 'Supports the variable {heading}.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000804 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100805 '--output-format-no-url', default='{title}',
806 help='Specifies the format to use when printing activity without url.')
807 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000808 '-m', '--markdown', action='store_true',
809 help='Use markdown-friendly output (overrides --output-format '
810 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000811 output_format_group.add_option(
812 '-j', '--json', action='store_true',
813 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000814 parser.add_option_group(output_format_group)
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()
Edward Lemur5b929a42019-10-21 17:57:39 +0000930 except auth.LoginRequiredError as e:
931 logging.error('auth.LoginRequiredError: %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)