blob: b4be6fd2041172692d4055e0859503c8fd74d873 [file] [log] [blame]
Edward Lesmeseb5dabf2020-02-28 19:44:17 +00001#!/usr/bin/env vpython
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Get stats about your activity.
7
8Example:
9 - my_activity.py for stats for the current week (last week on mondays).
10 - my_activity.py -Q for stats for last quarter.
11 - my_activity.py -Y for stats for this year.
Mohamed Heikaldc37feb2019-06-28 22:33:58 +000012 - my_activity.py -b 4/24/19 for stats since April 24th 2019.
13 - my_activity.py -b 4/24/19 -e 6/16/19 stats between April 24th and June 16th.
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +000014 - my_activity.py -jd to output stats for the week to json with deltas data.
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000015"""
16
17# These services typically only provide a created time and a last modified time
18# for each item for general queries. This is not enough to determine if there
19# was activity in a given time period. So, we first query for all things created
20# before end and modified after begin. Then, we get the details of each item and
21# check those details to determine if there was activity in the given period.
22# This means that query time scales mostly with (today() - begin).
23
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
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000045import gerrit_util
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000046
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +000047
Edward Lemur2a048032020-01-14 22:58:13 +000048if sys.version_info.major == 2:
49 import urllib as urllib_parse
50else:
51 import urllib.parse as urllib_parse
52
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000053try:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000054 import dateutil # pylint: disable=import-error
55 import dateutil.parser
56 from dateutil.relativedelta import relativedelta
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000057except ImportError:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000058 logging.error('python-dateutil package required')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000059 exit(1)
60
Tobias Sargeantffb3c432017-03-08 14:09:14 +000061
62class DefaultFormatter(Formatter):
63 def __init__(self, default = ''):
64 super(DefaultFormatter, self).__init__()
65 self.default = default
66
67 def get_value(self, key, args, kwds):
Edward Lemur2a048032020-01-14 22:58:13 +000068 if isinstance(key, str) and key not in kwds:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000069 return self.default
70 return Formatter.get_value(self, key, args, kwds)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000071
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000072gerrit_instances = [
73 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000074 'url': 'android-review.googlesource.com',
Mike Frysingerfdf027a2020-02-27 21:53:45 +000075 'shorturl': 'r.android.com',
76 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000077 },
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000078 {
Mike Frysinger24fd8632020-02-27 22:07:06 +000079 'url': 'gerrit-review.googlesource.com',
80 },
81 {
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000082 'url': 'chrome-internal-review.googlesource.com',
Jeremy Romanf475a472017-06-06 16:49:11 -040083 'shorturl': 'crrev.com/i',
Varun Khanejad9f97bc2017-08-02 12:55:01 -070084 'short_url_protocol': 'https',
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000085 },
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000086 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000087 'url': 'chromium-review.googlesource.com',
88 'shorturl': 'crrev.com/c',
89 'short_url_protocol': 'https',
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000090 },
Ryan Harrison897602a2017-09-18 16:23:41 -040091 {
Ryan Harrison06e18692019-09-23 18:22:25 +000092 'url': 'dawn-review.googlesource.com',
93 },
94 {
Ryan Harrison897602a2017-09-18 16:23:41 -040095 'url': 'pdfium-review.googlesource.com',
96 },
Adrienne Walker95d4c852018-09-27 20:28:12 +000097 {
98 'url': 'skia-review.googlesource.com',
99 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000100]
101
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100102monorail_projects = {
Jamie Madill1db68ea2019-09-03 19:34:42 +0000103 'angleproject': {
104 'shorturl': 'anglebug.com',
105 'short_url_protocol': 'http',
106 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100107 'chromium': {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000108 'shorturl': 'crbug.com',
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700109 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000110 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000111 'dawn': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100112 'google-breakpad': {},
113 'gyp': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100114 'pdfium': {
Ryan Harrison897602a2017-09-18 16:23:41 -0400115 'shorturl': 'crbug.com/pdfium',
116 'short_url_protocol': 'https',
117 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000118 'skia': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100119 'v8': {
120 'shorturl': 'crbug.com/v8',
121 'short_url_protocol': 'https',
122 },
123}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000124
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000125def username(email):
126 """Keeps the username of an email address."""
127 return email and email.split('@', 1)[0]
128
129
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000130def datetime_to_midnight(date):
131 return date - timedelta(hours=date.hour, minutes=date.minute,
132 seconds=date.second, microseconds=date.microsecond)
133
134
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000135def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000136 begin = (datetime_to_midnight(date) -
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000137 relativedelta(months=(date.month - 1) % 3, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000138 return begin, begin + relativedelta(months=3)
139
140
141def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000142 begin = (datetime_to_midnight(date) -
143 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000144 return begin, begin + relativedelta(years=1)
145
146
147def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000148 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000149 return begin, begin + timedelta(days=7)
150
151
152def get_yes_or_no(msg):
153 while True:
154 response = raw_input(msg + ' yes/no [no] ')
155 if response == 'y' or response == 'yes':
156 return True
157 elif not response or response == 'n' or response == 'no':
158 return False
159
160
deymo@chromium.org6c039202013-09-12 12:28:12 +0000161def datetime_from_gerrit(date_string):
162 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
163
164
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100165def datetime_from_monorail(date_string):
166 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000167
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000168def extract_bug_numbers_from_description(issue):
169 # Getting the description for REST Gerrit
170 revision = issue['revisions'][issue['current_revision']]
171 description = revision['commit']['message']
172
173 bugs = []
174 # Handle both "Bug: 99999" and "BUG=99999" bug notations
175 # Multiple bugs can be noted on a single line or in multiple ones.
176 matches = re.findall(
177 r'^(BUG=|(Bug|Fixed):\s*)((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)',
178 description, flags=re.IGNORECASE | re.MULTILINE)
179 if matches:
180 for match in matches:
181 bugs.extend(match[2].replace(' ', '').split(','))
182 # Add default chromium: prefix if none specified.
183 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
184
185 return sorted(set(bugs))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000186
187class MyActivity(object):
188 def __init__(self, options):
189 self.options = options
190 self.modified_after = options.begin
191 self.modified_before = options.end
192 self.user = options.user
193 self.changes = []
194 self.reviews = []
195 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100196 self.referenced_issues = []
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000197 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-14 19:02:55 -0700198 self.access_errors = set()
Vadim Bendebury80012972019-11-23 02:23:11 +0000199 self.skip_servers = (options.skip_servers.split(','))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000200
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100201 def show_progress(self, how='.'):
202 if sys.stdout.isatty():
203 sys.stdout.write(how)
204 sys.stdout.flush()
205
Vadim Bendebury8de38002018-05-14 19:02:55 -0700206 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200207 # Convert the "key:value" filter to a list of (key, value) pairs.
208 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000209 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000210 # Instantiate the generator to force all the requests now and catch the
211 # errors here.
212 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000213 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
214 'CURRENT_REVISION', 'CURRENT_COMMIT']))
Raul Tambre7c938462019-05-24 16:35:35 +0000215 except gerrit_util.GerritError as e:
Vadim Bendebury8de38002018-05-14 19:02:55 -0700216 error_message = 'Looking up %r: %s' % (instance['url'], e)
217 if error_message not in self.access_errors:
218 self.access_errors.add(error_message)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000219 return []
220
deymo@chromium.org6c039202013-09-12 12:28:12 +0000221 def gerrit_search(self, instance, owner=None, reviewer=None):
Vadim Bendebury80012972019-11-23 02:23:11 +0000222 if instance['url'] in self.skip_servers:
223 return []
deymo@chromium.org6c039202013-09-12 12:28:12 +0000224 max_age = datetime.today() - self.modified_after
Andrii Shyshkalov4aedec02018-09-17 16:31:17 +0000225 filters = ['-age:%ss' % (max_age.days * 24 * 3600 + max_age.seconds)]
226 if owner:
227 assert not reviewer
228 filters.append('owner:%s' % owner)
229 else:
230 filters.extend(('-owner:%s' % reviewer, 'reviewer:%s' % reviewer))
Peter K. Lee9342ac02018-08-28 21:03:51 +0000231 # TODO(cjhopman): Should abandoned changes be filtered out when
232 # merged_only is not enabled?
233 if self.options.merged_only:
234 filters.append('status:merged')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000235
Aaron Gable2979a872017-09-05 17:38:32 -0700236 issues = self.gerrit_changes_over_rest(instance, filters)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100237 self.show_progress()
Aaron Gable2979a872017-09-05 17:38:32 -0700238 issues = [self.process_gerrit_issue(instance, issue)
239 for issue in issues]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000240
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000241 issues = filter(self.filter_issue, issues)
242 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
243
244 return issues
245
Aaron Gable2979a872017-09-05 17:38:32 -0700246 def process_gerrit_issue(self, instance, issue):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000247 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000248 if self.options.deltas:
249 ret['delta'] = DefaultFormatter().format(
250 '+{insertions},-{deletions}',
251 **issue)
252 ret['status'] = issue['status']
deymo@chromium.org6c039202013-09-12 12:28:12 +0000253 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700254 protocol = instance.get('short_url_protocol', 'http')
255 url = instance['shorturl']
256 else:
257 protocol = 'https'
258 url = instance['url']
259 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
260
deymo@chromium.org6c039202013-09-12 12:28:12 +0000261 ret['header'] = issue['subject']
Don Garrett2ebf9fd2018-08-06 15:14:00 +0000262 ret['owner'] = issue['owner'].get('email', '')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000263 ret['author'] = ret['owner']
264 ret['created'] = datetime_from_gerrit(issue['created'])
265 ret['modified'] = datetime_from_gerrit(issue['updated'])
266 if 'messages' in issue:
Aaron Gable2979a872017-09-05 17:38:32 -0700267 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
deymo@chromium.org6c039202013-09-12 12:28:12 +0000268 else:
269 ret['replies'] = []
270 ret['reviewers'] = set(r['author'] for r in ret['replies'])
271 ret['reviewers'].discard(ret['author'])
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000272 ret['bugs'] = extract_bug_numbers_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000273 return ret
274
275 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700276 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000277 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000278 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
279 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000280 for reply in replies:
281 ret.append({
282 'author': reply['author']['email'],
283 'created': datetime_from_gerrit(reply['date']),
284 'content': reply['message'],
285 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000286 return ret
287
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100288 def monorail_get_auth_http(self):
Kenneth Russell66badbd2018-09-09 21:35:32 +0000289 # Manually use a long timeout (10m); for some users who have a
290 # long history on the issue tracker, whatever the default timeout
291 # is is reached.
Edward Lemur5b929a42019-10-21 17:57:39 +0000292 return auth.Authenticator().authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100293
294 def filter_modified_monorail_issue(self, issue):
295 """Precisely checks if an issue has been modified in the time range.
296
297 This fetches all issue comments to check if the issue has been modified in
298 the time range specified by user. This is needed because monorail only
299 allows filtering by last updated and published dates, which is not
300 sufficient to tell whether a given issue has been modified at some specific
301 time range. Any update to the issue is a reported as comment on Monorail.
302
303 Args:
304 issue: Issue dict as returned by monorail_query_issues method. In
305 particular, must have a key 'uid' formatted as 'project:issue_id'.
306
307 Returns:
308 Passed issue if modified, None otherwise.
309 """
310 http = self.monorail_get_auth_http()
311 project, issue_id = issue['uid'].split(':')
312 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
313 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
314 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100315 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100316 content = json.loads(body)
317 if not content:
318 logging.error('Unable to parse %s response from monorail.', project)
319 return issue
320
321 for item in content.get('items', []):
322 comment_published = datetime_from_monorail(item['published'])
323 if self.filter_modified(comment_published):
324 return issue
325
326 return None
327
328 def monorail_query_issues(self, project, query):
329 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000330 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100331 '/%s/issues') % project
Edward Lemur2a048032020-01-14 22:58:13 +0000332 query_data = urllib_parse.urlencode(query)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100333 url = url + '?' + query_data
334 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100335 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100336 content = json.loads(body)
337 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100338 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100339 return []
340
341 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100342 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100343 for item in content.get('items', []):
344 if project_config.get('shorturl'):
345 protocol = project_config.get('short_url_protocol', 'http')
346 item_url = '%s://%s/%d' % (
347 protocol, project_config['shorturl'], item['id'])
348 else:
349 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
350 project, item['id'])
351 issue = {
352 'uid': '%s:%s' % (project, item['id']),
353 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100354 'created': datetime_from_monorail(item['published']),
355 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100356 'author': item['author']['name'],
357 'url': item_url,
358 'comments': [],
359 'status': item['status'],
360 'labels': [],
361 'components': []
362 }
363 if 'owner' in item:
364 issue['owner'] = item['owner']['name']
365 else:
366 issue['owner'] = 'None'
367 if 'labels' in item:
368 issue['labels'] = item['labels']
369 if 'components' in item:
370 issue['components'] = item['components']
371 issues.append(issue)
372
373 return issues
374
375 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000376 epoch = datetime.utcfromtimestamp(0)
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000377 # Defaults to @chromium.org email if one wasn't provided on -u option.
378 user_str = (self.options.email if self.options.email.find('@') >= 0
379 else '%s@chromium.org' % self.user)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000380
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100381 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000382 'maxResults': 10000,
383 'q': user_str,
384 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
385 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000386 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000387
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000388 if self.options.completed_issues:
389 return [
390 issue for issue in issues
391 if (self.match(issue['owner']) and
392 issue['status'].lower() in ('verified', 'fixed'))
393 ]
394
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100395 return [
396 issue for issue in issues
397 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000398
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100399 def monorail_get_issues(self, project, issue_ids):
400 return self.monorail_query_issues(project, {
401 'maxResults': 10000,
402 'q': 'id:%s' % ','.join(issue_ids)
403 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000404
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000405 def print_heading(self, heading):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000406 print()
407 print(self.options.output_format_heading.format(heading=heading))
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000408
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000409 def match(self, author):
410 if '@' in self.user:
411 return author == self.user
412 return author.startswith(self.user + '@')
413
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000414 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000415 activity = len([
416 reply
417 for reply in change['replies']
418 if self.match(reply['author'])
419 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000420 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000421 'created': change['created'].date().isoformat(),
422 'modified': change['modified'].date().isoformat(),
423 'reviewers': ', '.join(change['reviewers']),
424 'status': change['status'],
425 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000426 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000427 if self.options.deltas:
428 optional_values['delta'] = change['delta']
429
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000430 self.print_generic(self.options.output_format,
431 self.options.output_format_changes,
432 change['header'],
433 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000434 change['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000435 change['created'],
436 change['modified'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000437 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000438
439 def print_issue(self, issue):
440 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000441 'created': issue['created'].date().isoformat(),
442 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000443 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000444 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000445 }
446 self.print_generic(self.options.output_format,
447 self.options.output_format_issues,
448 issue['header'],
449 issue['url'],
450 issue['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000451 issue['created'],
452 issue['modified'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000453 optional_values)
454
455 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000456 activity = len([
457 reply
458 for reply in review['replies']
459 if self.match(reply['author'])
460 ])
461 optional_values = {
462 'created': review['created'].date().isoformat(),
463 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800464 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000465 'activity': activity,
466 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800467 if self.options.deltas:
468 optional_values['delta'] = review['delta']
469
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000470 self.print_generic(self.options.output_format,
471 self.options.output_format_reviews,
472 review['header'],
473 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000474 review['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000475 review['created'],
476 review['modified'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000477 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000478
479 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000480 def print_generic(default_fmt, specific_fmt,
Lutz Justen860378e2019-03-12 01:10:02 +0000481 title, url, author, created, modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000482 optional_values=None):
483 output_format = specific_fmt if specific_fmt is not None else default_fmt
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000484 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000485 'title': title,
486 'url': url,
487 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000488 'created': created,
489 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000490 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000491 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000492 values.update(optional_values)
Edward Lemur2a048032020-01-14 22:58:13 +0000493 print(DefaultFormatter().format(output_format, **values))
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000494
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000495
496 def filter_issue(self, issue, should_filter_by_user=True):
497 def maybe_filter_username(email):
498 return not should_filter_by_user or username(email) == self.user
499 if (maybe_filter_username(issue['author']) and
500 self.filter_modified(issue['created'])):
501 return True
502 if (maybe_filter_username(issue['owner']) and
503 (self.filter_modified(issue['created']) or
504 self.filter_modified(issue['modified']))):
505 return True
506 for reply in issue['replies']:
507 if self.filter_modified(reply['created']):
508 if not should_filter_by_user:
509 break
510 if (username(reply['author']) == self.user
511 or (self.user + '@') in reply['content']):
512 break
513 else:
514 return False
515 return True
516
517 def filter_modified(self, modified):
518 return self.modified_after < modified and modified < self.modified_before
519
520 def auth_for_changes(self):
521 #TODO(cjhopman): Move authentication check for getting changes here.
522 pass
523
524 def auth_for_reviews(self):
525 # Reviews use all the same instances as changes so no authentication is
526 # required.
527 pass
528
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000529 def get_changes(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000530 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100531 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100532 gerrit_changes = pool.map_async(
533 lambda instance: self.gerrit_search(instance, owner=self.user),
534 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100535 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000536 self.changes = list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000537
538 def print_changes(self):
539 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000540 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000541 for change in self.changes:
Bruce Dawson2afcf222019-02-25 22:07:08 +0000542 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000543
Vadim Bendebury8de38002018-05-14 19:02:55 -0700544 def print_access_errors(self):
545 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400546 logging.error('Access Errors:')
547 for error in self.access_errors:
548 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700549
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000550 def get_reviews(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000551 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100552 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100553 gerrit_reviews = pool.map_async(
554 lambda instance: self.gerrit_search(instance, reviewer=self.user),
555 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100556 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000557 self.reviews = list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000558
559 def print_reviews(self):
560 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000561 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000562 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000563 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000564
565 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100566 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
567 monorail_issues = pool.map(
568 self.monorail_issue_search, monorail_projects.keys())
569 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
570
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700571 if not monorail_issues:
572 return
573
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100574 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
575 filtered_issues = pool.map(
576 self.filter_modified_monorail_issue, monorail_issues)
577 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100578
579 def get_referenced_issues(self):
580 if not self.issues:
581 self.get_issues()
582
583 if not self.changes:
584 self.get_changes()
585
586 referenced_issue_uids = set(itertools.chain.from_iterable(
587 change['bugs'] for change in self.changes))
588 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
589 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
590
591 missing_issues_by_project = collections.defaultdict(list)
592 for issue_uid in missing_issue_uids:
593 project, issue_id = issue_uid.split(':')
594 missing_issues_by_project[project].append(issue_id)
595
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000596 for project, issue_ids in missing_issues_by_project.items():
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100597 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000598
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000599 def print_issues(self):
600 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000601 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000602 for issue in self.issues:
603 self.print_issue(issue)
604
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100605 def print_changes_by_issue(self, skip_empty_own):
606 if not self.issues or not self.changes:
607 return
608
609 self.print_heading('Changes by referenced issue(s)')
610 issues = {issue['uid']: issue for issue in self.issues}
611 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
612 changes_by_issue_uid = collections.defaultdict(list)
613 changes_by_ref_issue_uid = collections.defaultdict(list)
614 changes_without_issue = []
615 for change in self.changes:
616 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000617 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100618 if issue_uid in issues:
619 changes_by_issue_uid[issue_uid].append(change)
620 added = True
621 if issue_uid in ref_issues:
622 changes_by_ref_issue_uid[issue_uid].append(change)
623 added = True
624 if not added:
625 changes_without_issue.append(change)
626
627 # Changes referencing own issues.
628 for issue_uid in issues:
629 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
630 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000631 if changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000632 print()
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000633 for change in changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000634 print(' ', end='') # this prints no newline
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000635 self.print_change(change)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000636 print()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100637
638 # Changes referencing others' issues.
639 for issue_uid in ref_issues:
640 assert changes_by_ref_issue_uid[issue_uid]
641 self.print_issue(ref_issues[issue_uid])
642 for change in changes_by_ref_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000643 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100644 self.print_change(change)
645
646 # Changes referencing no issues.
647 if changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000648 print(self.options.output_format_no_url.format(title='Other changes'))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100649 for change in changes_without_issue:
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
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000653 def print_activity(self):
654 self.print_changes()
655 self.print_reviews()
656 self.print_issues()
657
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000658 def dump_json(self, ignore_keys=None):
659 if ignore_keys is None:
660 ignore_keys = ['replies']
661
662 def format_for_json_dump(in_array):
663 output = {}
664 for item in in_array:
665 url = item.get('url') or item.get('review_url')
666 if not url:
667 raise Exception('Dumped item %s does not specify url' % item)
668 output[url] = dict(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000669 (k, v) for k,v in item.items() if k not in ignore_keys)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000670 return output
671
672 class PythonObjectEncoder(json.JSONEncoder):
673 def default(self, obj): # pylint: disable=method-hidden
674 if isinstance(obj, datetime):
675 return obj.isoformat()
676 if isinstance(obj, set):
677 return list(obj)
678 return json.JSONEncoder.default(self, obj)
679
680 output = {
681 'reviews': format_for_json_dump(self.reviews),
682 'changes': format_for_json_dump(self.changes),
683 'issues': format_for_json_dump(self.issues)
684 }
Raul Tambre80ee78e2019-05-06 22:41:05 +0000685 print(json.dumps(output, indent=2, cls=PythonObjectEncoder))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000686
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000687
688def main():
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000689 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
690 parser.add_option(
691 '-u', '--user', metavar='<email>',
Bruce Dawson84370962019-02-21 05:33:37 +0000692 # Look for USER and USERNAME (Windows) environment variables.
693 default=os.environ.get('USER', os.environ.get('USERNAME')),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000694 help='Filter on user, default=%default')
695 parser.add_option(
696 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000697 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000698 parser.add_option(
699 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000700 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000701 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
702 relativedelta(months=2))
703 parser.add_option(
704 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000705 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000706 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
707 parser.add_option(
708 '-Y', '--this_year', action='store_true',
709 help='Use this year\'s dates')
710 parser.add_option(
711 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000712 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000713 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000714 '-W', '--last_week', action='count',
715 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000716 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000717 '-a', '--auth',
718 action='store_true',
719 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000720 parser.add_option(
721 '-d', '--deltas',
722 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800723 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100724 parser.add_option(
725 '--no-referenced-issues',
726 action='store_true',
727 help='Do not fetch issues referenced by owned changes. Useful in '
728 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100729 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100730 parser.add_option(
Vadim Bendebury80012972019-11-23 02:23:11 +0000731 '--skip_servers',
732 action='store',
733 default='',
734 help='A comma separated list of gerrit and rietveld servers to ignore')
735 parser.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100736 '--skip-own-issues-without-changes',
737 action='store_true',
738 help='Skips listing own issues without changes when showing changes '
739 'grouped by referenced issue(s). See --changes-by-issue for more '
740 'details.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000741
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000742 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000743 'By default, all activity will be looked up and '
744 'printed. If any of these are specified, only '
745 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000746 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000747 '-c', '--changes',
748 action='store_true',
749 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000750 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000751 '-i', '--issues',
752 action='store_true',
753 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000754 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000755 '-r', '--reviews',
756 action='store_true',
757 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100758 activity_types_group.add_option(
759 '--changes-by-issue', action='store_true',
760 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000761 parser.add_option_group(activity_types_group)
762
763 output_format_group = optparse.OptionGroup(parser, 'Output Format',
764 'By default, all activity will be printed in the '
765 'following format: {url} {title}. This can be '
766 'changed for either all activity types or '
767 'individually for each activity type. The format '
768 'is defined as documented for '
769 'string.format(...). The variables available for '
Lutz Justen860378e2019-03-12 01:10:02 +0000770 'all activity types are url, title, author, '
771 'created and modified. Format options for '
772 'specific activity types will override the '
773 'generic format.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000774 output_format_group.add_option(
775 '-f', '--output-format', metavar='<format>',
776 default=u'{url} {title}',
777 help='Specifies the format to use when printing all your activity.')
778 output_format_group.add_option(
779 '--output-format-changes', metavar='<format>',
780 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000781 help='Specifies the format to use when printing changes. Supports the '
782 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000783 output_format_group.add_option(
784 '--output-format-issues', metavar='<format>',
785 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000786 help='Specifies the format to use when printing issues. Supports the '
787 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000788 output_format_group.add_option(
789 '--output-format-reviews', metavar='<format>',
790 default=None,
791 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000792 output_format_group.add_option(
793 '--output-format-heading', metavar='<format>',
794 default=u'{heading}:',
795 help='Specifies the format to use when printing headings.')
796 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100797 '--output-format-no-url', default='{title}',
798 help='Specifies the format to use when printing activity without url.')
799 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000800 '-m', '--markdown', action='store_true',
801 help='Use markdown-friendly output (overrides --output-format '
802 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000803 output_format_group.add_option(
804 '-j', '--json', action='store_true',
805 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000806 parser.add_option_group(output_format_group)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000807
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000808 parser.add_option(
809 '-v', '--verbose',
810 action='store_const',
811 dest='verbosity',
812 default=logging.WARN,
813 const=logging.INFO,
814 help='Output extra informational messages.'
815 )
816 parser.add_option(
817 '-q', '--quiet',
818 action='store_const',
819 dest='verbosity',
820 const=logging.ERROR,
821 help='Suppress non-error messages.'
822 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000823 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000824 '-M', '--merged-only',
825 action='store_true',
826 dest='merged_only',
827 default=False,
828 help='Shows only changes that have been merged.')
829 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000830 '-C', '--completed-issues',
831 action='store_true',
832 dest='completed_issues',
833 default=False,
834 help='Shows only monorail issues that have completed (Fixed|Verified) '
835 'by the user.')
836 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000837 '-o', '--output', metavar='<file>',
838 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000839
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000840 # Remove description formatting
841 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800842 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000843
844 options, args = parser.parse_args()
845 options.local_user = os.environ.get('USER')
846 if args:
847 parser.error('Args unsupported')
848 if not options.user:
Bruce Dawson84370962019-02-21 05:33:37 +0000849 parser.error('USER/USERNAME is not set, please use -u')
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000850 # Retains the original -u option as the email address.
851 options.email = options.user
852 options.user = username(options.email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000853
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000854 logging.basicConfig(level=options.verbosity)
855
856 # python-keyring provides easy access to the system keyring.
857 try:
858 import keyring # pylint: disable=unused-import,unused-variable,F0401
859 except ImportError:
860 logging.warning('Consider installing python-keyring')
861
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000862 if not options.begin:
863 if options.last_quarter:
864 begin, end = quarter_begin, quarter_end
865 elif options.this_year:
866 begin, end = get_year_of(datetime.today())
867 elif options.week_of:
868 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000869 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000870 begin, end = (get_week_of(datetime.today() -
871 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000872 else:
873 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
874 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700875 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000876 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700877 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000878 else:
879 end = datetime.today()
880 options.begin, options.end = begin, end
Bruce Dawson2afcf222019-02-25 22:07:08 +0000881 if begin >= end:
882 # The queries fail in peculiar ways when the begin date is in the future.
883 # Give a descriptive error message instead.
884 logging.error('Start date (%s) is the same or later than end date (%s)' %
885 (begin, end))
886 return 1
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000887
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000888 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000889 options.output_format_heading = '### {heading}\n'
890 options.output_format = ' * [{title}]({url})'
891 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000892 logging.info('Searching for activity by %s', options.user)
893 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000894
895 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100896 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000897
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100898 if not (options.changes or options.reviews or options.issues or
899 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000900 options.changes = True
901 options.issues = True
902 options.reviews = True
903
904 # First do any required authentication so none of the user interaction has to
905 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100906 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000907 my_activity.auth_for_changes()
908 if options.reviews:
909 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000910
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000911 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000912
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000913 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100914 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000915 my_activity.get_changes()
916 if options.reviews:
917 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100918 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000919 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100920 if not options.no_referenced_issues:
921 my_activity.get_referenced_issues()
Edward Lemur5b929a42019-10-21 17:57:39 +0000922 except auth.LoginRequiredError as e:
923 logging.error('auth.LoginRequiredError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000924
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100925 my_activity.show_progress('\n')
926
Vadim Bendebury8de38002018-05-14 19:02:55 -0700927 my_activity.print_access_errors()
928
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000929 output_file = None
930 try:
931 if options.output:
932 output_file = open(options.output, 'w')
933 logging.info('Printing output to "%s"', options.output)
934 sys.stdout = output_file
935 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700936 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000937 else:
938 if options.json:
939 my_activity.dump_json()
940 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100941 if options.changes:
942 my_activity.print_changes()
943 if options.reviews:
944 my_activity.print_reviews()
945 if options.issues:
946 my_activity.print_issues()
947 if options.changes_by_issue:
948 my_activity.print_changes_by_issue(
949 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000950 finally:
951 if output_file:
952 logging.info('Done printing to file.')
953 sys.stdout = sys.__stdout__
954 output_file.close()
955
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000956 return 0
957
958
959if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000960 # Fix encoding to support non-ascii issue titles.
961 fix_encoding.fix_encoding()
962
sbc@chromium.org013731e2015-02-26 18:28:43 +0000963 try:
964 sys.exit(main())
965 except KeyboardInterrupt:
966 sys.stderr.write('interrupted\n')
967 sys.exit(1)