blob: 71c0bc2f26ee44b66e38040fb3b13def05f060ad [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 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000103]
104
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100105monorail_projects = {
Jamie Madill1db68ea2019-09-03 19:34:42 +0000106 'angleproject': {
107 'shorturl': 'anglebug.com',
108 'short_url_protocol': 'http',
109 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100110 'chromium': {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000111 'shorturl': 'crbug.com',
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700112 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000113 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000114 'dawn': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100115 'google-breakpad': {},
116 'gyp': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100117 'pdfium': {
Ryan Harrison897602a2017-09-18 16:23:41 -0400118 'shorturl': 'crbug.com/pdfium',
119 'short_url_protocol': 'https',
120 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000121 'skia': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100122 'v8': {
123 'shorturl': 'crbug.com/v8',
124 'short_url_protocol': 'https',
125 },
126}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000127
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000128def username(email):
129 """Keeps the username of an email address."""
130 return email and email.split('@', 1)[0]
131
132
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000133def datetime_to_midnight(date):
134 return date - timedelta(hours=date.hour, minutes=date.minute,
135 seconds=date.second, microseconds=date.microsecond)
136
137
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000138def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000139 begin = (datetime_to_midnight(date) -
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000140 relativedelta(months=(date.month - 1) % 3, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000141 return begin, begin + relativedelta(months=3)
142
143
144def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000145 begin = (datetime_to_midnight(date) -
146 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000147 return begin, begin + relativedelta(years=1)
148
149
150def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000151 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000152 return begin, begin + timedelta(days=7)
153
154
155def get_yes_or_no(msg):
156 while True:
Edward Lesmesae3586b2020-03-23 21:21:14 +0000157 response = gclient_utils.AskForData(msg + ' yes/no [no] ')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000158 if response == 'y' or response == 'yes':
159 return True
160 elif not response or response == 'n' or response == 'no':
161 return False
162
163
deymo@chromium.org6c039202013-09-12 12:28:12 +0000164def datetime_from_gerrit(date_string):
165 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
166
167
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100168def datetime_from_monorail(date_string):
169 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000170
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000171def extract_bug_numbers_from_description(issue):
172 # Getting the description for REST Gerrit
173 revision = issue['revisions'][issue['current_revision']]
174 description = revision['commit']['message']
175
176 bugs = []
177 # Handle both "Bug: 99999" and "BUG=99999" bug notations
178 # Multiple bugs can be noted on a single line or in multiple ones.
179 matches = re.findall(
180 r'^(BUG=|(Bug|Fixed):\s*)((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)',
181 description, flags=re.IGNORECASE | re.MULTILINE)
182 if matches:
183 for match in matches:
184 bugs.extend(match[2].replace(' ', '').split(','))
185 # Add default chromium: prefix if none specified.
186 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
187
188 return sorted(set(bugs))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000189
190class MyActivity(object):
191 def __init__(self, options):
192 self.options = options
193 self.modified_after = options.begin
194 self.modified_before = options.end
195 self.user = options.user
196 self.changes = []
197 self.reviews = []
198 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100199 self.referenced_issues = []
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000200 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-14 19:02:55 -0700201 self.access_errors = set()
Vadim Bendebury80012972019-11-23 02:23:11 +0000202 self.skip_servers = (options.skip_servers.split(','))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000203
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100204 def show_progress(self, how='.'):
205 if sys.stdout.isatty():
206 sys.stdout.write(how)
207 sys.stdout.flush()
208
Vadim Bendebury8de38002018-05-14 19:02:55 -0700209 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200210 # Convert the "key:value" filter to a list of (key, value) pairs.
211 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000212 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000213 # Instantiate the generator to force all the requests now and catch the
214 # errors here.
215 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000216 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
217 'CURRENT_REVISION', 'CURRENT_COMMIT']))
Raul Tambre7c938462019-05-24 16:35:35 +0000218 except gerrit_util.GerritError as e:
Vadim Bendebury8de38002018-05-14 19:02:55 -0700219 error_message = 'Looking up %r: %s' % (instance['url'], e)
220 if error_message not in self.access_errors:
221 self.access_errors.add(error_message)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000222 return []
223
deymo@chromium.org6c039202013-09-12 12:28:12 +0000224 def gerrit_search(self, instance, owner=None, reviewer=None):
Vadim Bendebury80012972019-11-23 02:23:11 +0000225 if instance['url'] in self.skip_servers:
226 return []
deymo@chromium.org6c039202013-09-12 12:28:12 +0000227 max_age = datetime.today() - self.modified_after
Andrii Shyshkalov4aedec02018-09-17 16:31:17 +0000228 filters = ['-age:%ss' % (max_age.days * 24 * 3600 + max_age.seconds)]
229 if owner:
230 assert not reviewer
231 filters.append('owner:%s' % owner)
232 else:
233 filters.extend(('-owner:%s' % reviewer, 'reviewer:%s' % reviewer))
Peter K. Lee9342ac02018-08-28 21:03:51 +0000234 # TODO(cjhopman): Should abandoned changes be filtered out when
235 # merged_only is not enabled?
236 if self.options.merged_only:
237 filters.append('status:merged')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000238
Aaron Gable2979a872017-09-05 17:38:32 -0700239 issues = self.gerrit_changes_over_rest(instance, filters)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100240 self.show_progress()
Aaron Gable2979a872017-09-05 17:38:32 -0700241 issues = [self.process_gerrit_issue(instance, issue)
242 for issue in issues]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000243
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000244 issues = filter(self.filter_issue, issues)
245 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
246
247 return issues
248
Aaron Gable2979a872017-09-05 17:38:32 -0700249 def process_gerrit_issue(self, instance, issue):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000250 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000251 if self.options.deltas:
252 ret['delta'] = DefaultFormatter().format(
253 '+{insertions},-{deletions}',
254 **issue)
255 ret['status'] = issue['status']
deymo@chromium.org6c039202013-09-12 12:28:12 +0000256 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700257 protocol = instance.get('short_url_protocol', 'http')
258 url = instance['shorturl']
259 else:
260 protocol = 'https'
261 url = instance['url']
262 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
263
deymo@chromium.org6c039202013-09-12 12:28:12 +0000264 ret['header'] = issue['subject']
Don Garrett2ebf9fd2018-08-06 15:14:00 +0000265 ret['owner'] = issue['owner'].get('email', '')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000266 ret['author'] = ret['owner']
267 ret['created'] = datetime_from_gerrit(issue['created'])
268 ret['modified'] = datetime_from_gerrit(issue['updated'])
269 if 'messages' in issue:
Aaron Gable2979a872017-09-05 17:38:32 -0700270 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
deymo@chromium.org6c039202013-09-12 12:28:12 +0000271 else:
272 ret['replies'] = []
273 ret['reviewers'] = set(r['author'] for r in ret['replies'])
274 ret['reviewers'].discard(ret['author'])
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000275 ret['bugs'] = extract_bug_numbers_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000276 return ret
277
278 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700279 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000280 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000281 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
282 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000283 for reply in replies:
284 ret.append({
285 'author': reply['author']['email'],
286 'created': datetime_from_gerrit(reply['date']),
287 'content': reply['message'],
288 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000289 return ret
290
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100291 def monorail_get_auth_http(self):
Kenneth Russell66badbd2018-09-09 21:35:32 +0000292 # Manually use a long timeout (10m); for some users who have a
293 # long history on the issue tracker, whatever the default timeout
294 # is is reached.
Edward Lemur5b929a42019-10-21 17:57:39 +0000295 return auth.Authenticator().authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100296
297 def filter_modified_monorail_issue(self, issue):
298 """Precisely checks if an issue has been modified in the time range.
299
300 This fetches all issue comments to check if the issue has been modified in
301 the time range specified by user. This is needed because monorail only
302 allows filtering by last updated and published dates, which is not
303 sufficient to tell whether a given issue has been modified at some specific
304 time range. Any update to the issue is a reported as comment on Monorail.
305
306 Args:
307 issue: Issue dict as returned by monorail_query_issues method. In
308 particular, must have a key 'uid' formatted as 'project:issue_id'.
309
310 Returns:
311 Passed issue if modified, None otherwise.
312 """
313 http = self.monorail_get_auth_http()
314 project, issue_id = issue['uid'].split(':')
315 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
316 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
317 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100318 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100319 content = json.loads(body)
320 if not content:
321 logging.error('Unable to parse %s response from monorail.', project)
322 return issue
323
324 for item in content.get('items', []):
325 comment_published = datetime_from_monorail(item['published'])
326 if self.filter_modified(comment_published):
327 return issue
328
329 return None
330
331 def monorail_query_issues(self, project, query):
332 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000333 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100334 '/%s/issues') % project
Edward Lemur2a048032020-01-14 22:58:13 +0000335 query_data = urllib_parse.urlencode(query)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100336 url = url + '?' + query_data
337 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100338 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100339 content = json.loads(body)
340 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100341 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100342 return []
343
344 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100345 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100346 for item in content.get('items', []):
347 if project_config.get('shorturl'):
348 protocol = project_config.get('short_url_protocol', 'http')
349 item_url = '%s://%s/%d' % (
350 protocol, project_config['shorturl'], item['id'])
351 else:
352 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
353 project, item['id'])
354 issue = {
355 'uid': '%s:%s' % (project, item['id']),
356 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100357 'created': datetime_from_monorail(item['published']),
358 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100359 'author': item['author']['name'],
360 'url': item_url,
361 'comments': [],
362 'status': item['status'],
363 'labels': [],
364 'components': []
365 }
366 if 'owner' in item:
367 issue['owner'] = item['owner']['name']
368 else:
369 issue['owner'] = 'None'
370 if 'labels' in item:
371 issue['labels'] = item['labels']
372 if 'components' in item:
373 issue['components'] = item['components']
374 issues.append(issue)
375
376 return issues
377
378 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000379 epoch = datetime.utcfromtimestamp(0)
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000380 # Defaults to @chromium.org email if one wasn't provided on -u option.
381 user_str = (self.options.email if self.options.email.find('@') >= 0
382 else '%s@chromium.org' % self.user)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000383
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100384 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000385 'maxResults': 10000,
386 'q': user_str,
387 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
388 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000389 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000390
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000391 if self.options.completed_issues:
392 return [
393 issue for issue in issues
394 if (self.match(issue['owner']) and
395 issue['status'].lower() in ('verified', 'fixed'))
396 ]
397
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100398 return [
399 issue for issue in issues
400 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000401
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100402 def monorail_get_issues(self, project, issue_ids):
403 return self.monorail_query_issues(project, {
404 'maxResults': 10000,
405 'q': 'id:%s' % ','.join(issue_ids)
406 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000407
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000408 def print_heading(self, heading):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000409 print()
410 print(self.options.output_format_heading.format(heading=heading))
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000411
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000412 def match(self, author):
413 if '@' in self.user:
414 return author == self.user
415 return author.startswith(self.user + '@')
416
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000417 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000418 activity = len([
419 reply
420 for reply in change['replies']
421 if self.match(reply['author'])
422 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000423 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000424 'created': change['created'].date().isoformat(),
425 'modified': change['modified'].date().isoformat(),
426 'reviewers': ', '.join(change['reviewers']),
427 'status': change['status'],
428 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000429 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000430 if self.options.deltas:
431 optional_values['delta'] = change['delta']
432
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000433 self.print_generic(self.options.output_format,
434 self.options.output_format_changes,
435 change['header'],
436 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000437 change['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000438 change['created'],
439 change['modified'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000440 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000441
442 def print_issue(self, issue):
443 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000444 'created': issue['created'].date().isoformat(),
445 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000446 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000447 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000448 }
449 self.print_generic(self.options.output_format,
450 self.options.output_format_issues,
451 issue['header'],
452 issue['url'],
453 issue['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000454 issue['created'],
455 issue['modified'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000456 optional_values)
457
458 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000459 activity = len([
460 reply
461 for reply in review['replies']
462 if self.match(reply['author'])
463 ])
464 optional_values = {
465 'created': review['created'].date().isoformat(),
466 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800467 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000468 'activity': activity,
469 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800470 if self.options.deltas:
471 optional_values['delta'] = review['delta']
472
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000473 self.print_generic(self.options.output_format,
474 self.options.output_format_reviews,
475 review['header'],
476 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000477 review['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000478 review['created'],
479 review['modified'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000480 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000481
482 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000483 def print_generic(default_fmt, specific_fmt,
Lutz Justen860378e2019-03-12 01:10:02 +0000484 title, url, author, created, modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000485 optional_values=None):
486 output_format = specific_fmt if specific_fmt is not None else default_fmt
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000487 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000488 'title': title,
489 'url': url,
490 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000491 'created': created,
492 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000493 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000494 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000495 values.update(optional_values)
Edward Lemur2a048032020-01-14 22:58:13 +0000496 print(DefaultFormatter().format(output_format, **values))
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000497
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000498
499 def filter_issue(self, issue, should_filter_by_user=True):
500 def maybe_filter_username(email):
501 return not should_filter_by_user or username(email) == self.user
502 if (maybe_filter_username(issue['author']) and
503 self.filter_modified(issue['created'])):
504 return True
505 if (maybe_filter_username(issue['owner']) and
506 (self.filter_modified(issue['created']) or
507 self.filter_modified(issue['modified']))):
508 return True
509 for reply in issue['replies']:
510 if self.filter_modified(reply['created']):
511 if not should_filter_by_user:
512 break
513 if (username(reply['author']) == self.user
514 or (self.user + '@') in reply['content']):
515 break
516 else:
517 return False
518 return True
519
520 def filter_modified(self, modified):
521 return self.modified_after < modified and modified < self.modified_before
522
523 def auth_for_changes(self):
524 #TODO(cjhopman): Move authentication check for getting changes here.
525 pass
526
527 def auth_for_reviews(self):
528 # Reviews use all the same instances as changes so no authentication is
529 # required.
530 pass
531
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000532 def get_changes(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000533 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100534 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100535 gerrit_changes = pool.map_async(
536 lambda instance: self.gerrit_search(instance, owner=self.user),
537 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100538 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000539 self.changes = list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000540
541 def print_changes(self):
542 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000543 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000544 for change in self.changes:
Bruce Dawson2afcf222019-02-25 22:07:08 +0000545 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000546
Vadim Bendebury8de38002018-05-14 19:02:55 -0700547 def print_access_errors(self):
548 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400549 logging.error('Access Errors:')
550 for error in self.access_errors:
551 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700552
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000553 def get_reviews(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000554 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100555 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100556 gerrit_reviews = pool.map_async(
557 lambda instance: self.gerrit_search(instance, reviewer=self.user),
558 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100559 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000560 self.reviews = list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000561
562 def print_reviews(self):
563 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000564 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000565 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000566 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000567
568 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100569 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
570 monorail_issues = pool.map(
571 self.monorail_issue_search, monorail_projects.keys())
572 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
573
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700574 if not monorail_issues:
575 return
576
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100577 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
578 filtered_issues = pool.map(
579 self.filter_modified_monorail_issue, monorail_issues)
580 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100581
582 def get_referenced_issues(self):
583 if not self.issues:
584 self.get_issues()
585
586 if not self.changes:
587 self.get_changes()
588
589 referenced_issue_uids = set(itertools.chain.from_iterable(
590 change['bugs'] for change in self.changes))
591 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
592 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
593
594 missing_issues_by_project = collections.defaultdict(list)
595 for issue_uid in missing_issue_uids:
596 project, issue_id = issue_uid.split(':')
597 missing_issues_by_project[project].append(issue_id)
598
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000599 for project, issue_ids in missing_issues_by_project.items():
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100600 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000601
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000602 def print_issues(self):
603 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000604 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000605 for issue in self.issues:
606 self.print_issue(issue)
607
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100608 def print_changes_by_issue(self, skip_empty_own):
609 if not self.issues or not self.changes:
610 return
611
612 self.print_heading('Changes by referenced issue(s)')
613 issues = {issue['uid']: issue for issue in self.issues}
614 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
615 changes_by_issue_uid = collections.defaultdict(list)
616 changes_by_ref_issue_uid = collections.defaultdict(list)
617 changes_without_issue = []
618 for change in self.changes:
619 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000620 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100621 if issue_uid in issues:
622 changes_by_issue_uid[issue_uid].append(change)
623 added = True
624 if issue_uid in ref_issues:
625 changes_by_ref_issue_uid[issue_uid].append(change)
626 added = True
627 if not added:
628 changes_without_issue.append(change)
629
630 # Changes referencing own issues.
631 for issue_uid in issues:
632 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
633 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000634 if changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000635 print()
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000636 for change in changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000637 print(' ', end='') # this prints no newline
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000638 self.print_change(change)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000639 print()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100640
641 # Changes referencing others' issues.
642 for issue_uid in ref_issues:
643 assert changes_by_ref_issue_uid[issue_uid]
644 self.print_issue(ref_issues[issue_uid])
645 for change in changes_by_ref_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000646 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100647 self.print_change(change)
648
649 # Changes referencing no issues.
650 if changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000651 print(self.options.output_format_no_url.format(title='Other changes'))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100652 for change in changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000653 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100654 self.print_change(change)
655
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000656 def print_activity(self):
657 self.print_changes()
658 self.print_reviews()
659 self.print_issues()
660
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000661 def dump_json(self, ignore_keys=None):
662 if ignore_keys is None:
663 ignore_keys = ['replies']
664
665 def format_for_json_dump(in_array):
666 output = {}
667 for item in in_array:
668 url = item.get('url') or item.get('review_url')
669 if not url:
670 raise Exception('Dumped item %s does not specify url' % item)
671 output[url] = dict(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000672 (k, v) for k,v in item.items() if k not in ignore_keys)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000673 return output
674
675 class PythonObjectEncoder(json.JSONEncoder):
676 def default(self, obj): # pylint: disable=method-hidden
677 if isinstance(obj, datetime):
678 return obj.isoformat()
679 if isinstance(obj, set):
680 return list(obj)
681 return json.JSONEncoder.default(self, obj)
682
683 output = {
684 'reviews': format_for_json_dump(self.reviews),
685 'changes': format_for_json_dump(self.changes),
686 'issues': format_for_json_dump(self.issues)
687 }
Raul Tambre80ee78e2019-05-06 22:41:05 +0000688 print(json.dumps(output, indent=2, cls=PythonObjectEncoder))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000689
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000690
691def main():
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000692 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
693 parser.add_option(
694 '-u', '--user', metavar='<email>',
Bruce Dawson84370962019-02-21 05:33:37 +0000695 # Look for USER and USERNAME (Windows) environment variables.
696 default=os.environ.get('USER', os.environ.get('USERNAME')),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000697 help='Filter on user, default=%default')
698 parser.add_option(
699 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000700 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000701 parser.add_option(
702 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000703 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000704 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
705 relativedelta(months=2))
706 parser.add_option(
707 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000708 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000709 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
710 parser.add_option(
711 '-Y', '--this_year', action='store_true',
712 help='Use this year\'s dates')
713 parser.add_option(
714 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000715 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000716 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000717 '-W', '--last_week', action='count',
718 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000719 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000720 '-a', '--auth',
721 action='store_true',
722 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000723 parser.add_option(
724 '-d', '--deltas',
725 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800726 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100727 parser.add_option(
728 '--no-referenced-issues',
729 action='store_true',
730 help='Do not fetch issues referenced by owned changes. Useful in '
731 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100732 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100733 parser.add_option(
Vadim Bendebury80012972019-11-23 02:23:11 +0000734 '--skip_servers',
735 action='store',
736 default='',
737 help='A comma separated list of gerrit and rietveld servers to ignore')
738 parser.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100739 '--skip-own-issues-without-changes',
740 action='store_true',
741 help='Skips listing own issues without changes when showing changes '
742 'grouped by referenced issue(s). See --changes-by-issue for more '
743 'details.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000744
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000745 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000746 'By default, all activity will be looked up and '
747 'printed. If any of these are specified, only '
748 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000749 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000750 '-c', '--changes',
751 action='store_true',
752 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000753 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000754 '-i', '--issues',
755 action='store_true',
756 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000757 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000758 '-r', '--reviews',
759 action='store_true',
760 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100761 activity_types_group.add_option(
762 '--changes-by-issue', action='store_true',
763 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000764 parser.add_option_group(activity_types_group)
765
766 output_format_group = optparse.OptionGroup(parser, 'Output Format',
767 'By default, all activity will be printed in the '
768 'following format: {url} {title}. This can be '
769 'changed for either all activity types or '
770 'individually for each activity type. The format '
771 'is defined as documented for '
772 'string.format(...). The variables available for '
Lutz Justen860378e2019-03-12 01:10:02 +0000773 'all activity types are url, title, author, '
774 'created and modified. Format options for '
775 'specific activity types will override the '
776 'generic format.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000777 output_format_group.add_option(
778 '-f', '--output-format', metavar='<format>',
779 default=u'{url} {title}',
780 help='Specifies the format to use when printing all your activity.')
781 output_format_group.add_option(
782 '--output-format-changes', metavar='<format>',
783 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000784 help='Specifies the format to use when printing changes. Supports the '
785 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000786 output_format_group.add_option(
787 '--output-format-issues', metavar='<format>',
788 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000789 help='Specifies the format to use when printing issues. Supports the '
790 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000791 output_format_group.add_option(
792 '--output-format-reviews', metavar='<format>',
793 default=None,
794 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000795 output_format_group.add_option(
796 '--output-format-heading', metavar='<format>',
797 default=u'{heading}:',
Vincent Scheib2be61a12020-05-26 16:45:32 +0000798 help='Specifies the format to use when printing headings. '
799 'Supports the variable {heading}.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000800 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100801 '--output-format-no-url', default='{title}',
802 help='Specifies the format to use when printing activity without url.')
803 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000804 '-m', '--markdown', action='store_true',
805 help='Use markdown-friendly output (overrides --output-format '
806 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000807 output_format_group.add_option(
808 '-j', '--json', action='store_true',
809 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000810 parser.add_option_group(output_format_group)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000811
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000812 parser.add_option(
813 '-v', '--verbose',
814 action='store_const',
815 dest='verbosity',
816 default=logging.WARN,
817 const=logging.INFO,
818 help='Output extra informational messages.'
819 )
820 parser.add_option(
821 '-q', '--quiet',
822 action='store_const',
823 dest='verbosity',
824 const=logging.ERROR,
825 help='Suppress non-error messages.'
826 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000827 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000828 '-M', '--merged-only',
829 action='store_true',
830 dest='merged_only',
831 default=False,
832 help='Shows only changes that have been merged.')
833 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000834 '-C', '--completed-issues',
835 action='store_true',
836 dest='completed_issues',
837 default=False,
838 help='Shows only monorail issues that have completed (Fixed|Verified) '
839 'by the user.')
840 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000841 '-o', '--output', metavar='<file>',
842 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000843
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000844 # Remove description formatting
845 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800846 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000847
848 options, args = parser.parse_args()
849 options.local_user = os.environ.get('USER')
850 if args:
851 parser.error('Args unsupported')
852 if not options.user:
Bruce Dawson84370962019-02-21 05:33:37 +0000853 parser.error('USER/USERNAME is not set, please use -u')
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000854 # Retains the original -u option as the email address.
855 options.email = options.user
856 options.user = username(options.email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000857
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000858 logging.basicConfig(level=options.verbosity)
859
860 # python-keyring provides easy access to the system keyring.
861 try:
862 import keyring # pylint: disable=unused-import,unused-variable,F0401
863 except ImportError:
864 logging.warning('Consider installing python-keyring')
865
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000866 if not options.begin:
867 if options.last_quarter:
868 begin, end = quarter_begin, quarter_end
869 elif options.this_year:
870 begin, end = get_year_of(datetime.today())
871 elif options.week_of:
872 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000873 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000874 begin, end = (get_week_of(datetime.today() -
875 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000876 else:
877 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
878 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700879 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000880 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700881 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000882 else:
883 end = datetime.today()
884 options.begin, options.end = begin, end
Bruce Dawson2afcf222019-02-25 22:07:08 +0000885 if begin >= end:
886 # The queries fail in peculiar ways when the begin date is in the future.
887 # Give a descriptive error message instead.
888 logging.error('Start date (%s) is the same or later than end date (%s)' %
889 (begin, end))
890 return 1
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000891
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000892 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000893 options.output_format_heading = '### {heading}\n'
894 options.output_format = ' * [{title}]({url})'
895 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000896 logging.info('Searching for activity by %s', options.user)
897 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000898
899 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100900 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000901
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100902 if not (options.changes or options.reviews or options.issues or
903 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000904 options.changes = True
905 options.issues = True
906 options.reviews = True
907
908 # First do any required authentication so none of the user interaction has to
909 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100910 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000911 my_activity.auth_for_changes()
912 if options.reviews:
913 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000914
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000915 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000916
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000917 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100918 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000919 my_activity.get_changes()
920 if options.reviews:
921 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100922 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000923 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100924 if not options.no_referenced_issues:
925 my_activity.get_referenced_issues()
Edward Lemur5b929a42019-10-21 17:57:39 +0000926 except auth.LoginRequiredError as e:
927 logging.error('auth.LoginRequiredError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000928
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100929 my_activity.show_progress('\n')
930
Vadim Bendebury8de38002018-05-14 19:02:55 -0700931 my_activity.print_access_errors()
932
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000933 output_file = None
934 try:
935 if options.output:
936 output_file = open(options.output, 'w')
937 logging.info('Printing output to "%s"', options.output)
938 sys.stdout = output_file
939 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700940 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000941 else:
942 if options.json:
943 my_activity.dump_json()
944 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100945 if options.changes:
946 my_activity.print_changes()
947 if options.reviews:
948 my_activity.print_reviews()
949 if options.issues:
950 my_activity.print_issues()
951 if options.changes_by_issue:
952 my_activity.print_changes_by_issue(
953 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000954 finally:
955 if output_file:
956 logging.info('Done printing to file.')
957 sys.stdout = sys.__stdout__
958 output_file.close()
959
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000960 return 0
961
962
963if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000964 # Fix encoding to support non-ascii issue titles.
965 fix_encoding.fix_encoding()
966
sbc@chromium.org013731e2015-02-26 18:28:43 +0000967 try:
968 sys.exit(main())
969 except KeyboardInterrupt:
970 sys.stderr.write('interrupted\n')
971 sys.exit(1)