blob: 0904b2c0a9e0dbe5046dc3f1568cdccf35cfa1eb [file] [log] [blame]
Edward Lemur488712a2020-02-28 00:46:05 +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
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:
Edward Lemur488712a2020-02-28 00:46:05 +000049 logging.warning(
50 'Python 2 is deprecated. Run my_activity.py using vpython3.')
Edward Lemur2a048032020-01-14 22:58:13 +000051 import urllib as urllib_parse
52else:
53 import urllib.parse as urllib_parse
54
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000055try:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000056 import dateutil # pylint: disable=import-error
57 import dateutil.parser
58 from dateutil.relativedelta import relativedelta
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000059except ImportError:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000060 logging.error('python-dateutil package required')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000061 exit(1)
62
Tobias Sargeantffb3c432017-03-08 14:09:14 +000063
64class DefaultFormatter(Formatter):
65 def __init__(self, default = ''):
66 super(DefaultFormatter, self).__init__()
67 self.default = default
68
69 def get_value(self, key, args, kwds):
Edward Lemur2a048032020-01-14 22:58:13 +000070 if isinstance(key, str) and key not in kwds:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000071 return self.default
72 return Formatter.get_value(self, key, args, kwds)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000073
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000074gerrit_instances = [
75 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000076 'url': 'android-review.googlesource.com',
Mike Frysingerfdf027a2020-02-27 21:53:45 +000077 'shorturl': 'r.android.com',
78 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000079 },
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000080 {
Mike Frysinger24fd8632020-02-27 22:07:06 +000081 'url': 'gerrit-review.googlesource.com',
82 },
83 {
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000084 'url': 'chrome-internal-review.googlesource.com',
Jeremy Romanf475a472017-06-06 16:49:11 -040085 'shorturl': 'crrev.com/i',
Varun Khanejad9f97bc2017-08-02 12:55:01 -070086 'short_url_protocol': 'https',
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000087 },
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000088 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000089 'url': 'chromium-review.googlesource.com',
90 'shorturl': 'crrev.com/c',
91 'short_url_protocol': 'https',
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000092 },
Ryan Harrison897602a2017-09-18 16:23:41 -040093 {
Ryan Harrison06e18692019-09-23 18:22:25 +000094 'url': 'dawn-review.googlesource.com',
95 },
96 {
Ryan Harrison897602a2017-09-18 16:23:41 -040097 'url': 'pdfium-review.googlesource.com',
98 },
Adrienne Walker95d4c852018-09-27 20:28:12 +000099 {
100 'url': 'skia-review.googlesource.com',
101 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000102]
103
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100104monorail_projects = {
Jamie Madill1db68ea2019-09-03 19:34:42 +0000105 'angleproject': {
106 'shorturl': 'anglebug.com',
107 'short_url_protocol': 'http',
108 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100109 'chromium': {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000110 'shorturl': 'crbug.com',
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700111 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000112 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000113 'dawn': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100114 'google-breakpad': {},
115 'gyp': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100116 'pdfium': {
Ryan Harrison897602a2017-09-18 16:23:41 -0400117 'shorturl': 'crbug.com/pdfium',
118 'short_url_protocol': 'https',
119 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000120 'skia': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100121 'v8': {
122 'shorturl': 'crbug.com/v8',
123 'short_url_protocol': 'https',
124 },
125}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000126
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000127def username(email):
128 """Keeps the username of an email address."""
129 return email and email.split('@', 1)[0]
130
131
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000132def datetime_to_midnight(date):
133 return date - timedelta(hours=date.hour, minutes=date.minute,
134 seconds=date.second, microseconds=date.microsecond)
135
136
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000137def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000138 begin = (datetime_to_midnight(date) -
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000139 relativedelta(months=(date.month - 1) % 3, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000140 return begin, begin + relativedelta(months=3)
141
142
143def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000144 begin = (datetime_to_midnight(date) -
145 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000146 return begin, begin + relativedelta(years=1)
147
148
149def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000150 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000151 return begin, begin + timedelta(days=7)
152
153
154def get_yes_or_no(msg):
155 while True:
156 response = raw_input(msg + ' yes/no [no] ')
157 if response == 'y' or response == 'yes':
158 return True
159 elif not response or response == 'n' or response == 'no':
160 return False
161
162
deymo@chromium.org6c039202013-09-12 12:28:12 +0000163def datetime_from_gerrit(date_string):
164 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
165
166
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100167def datetime_from_monorail(date_string):
168 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000169
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000170def extract_bug_numbers_from_description(issue):
171 # Getting the description for REST Gerrit
172 revision = issue['revisions'][issue['current_revision']]
173 description = revision['commit']['message']
174
175 bugs = []
176 # Handle both "Bug: 99999" and "BUG=99999" bug notations
177 # Multiple bugs can be noted on a single line or in multiple ones.
178 matches = re.findall(
179 r'^(BUG=|(Bug|Fixed):\s*)((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)',
180 description, flags=re.IGNORECASE | re.MULTILINE)
181 if matches:
182 for match in matches:
183 bugs.extend(match[2].replace(' ', '').split(','))
184 # Add default chromium: prefix if none specified.
185 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
186
187 return sorted(set(bugs))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000188
189class MyActivity(object):
190 def __init__(self, options):
191 self.options = options
192 self.modified_after = options.begin
193 self.modified_before = options.end
194 self.user = options.user
195 self.changes = []
196 self.reviews = []
197 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100198 self.referenced_issues = []
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000199 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-14 19:02:55 -0700200 self.access_errors = set()
Vadim Bendebury80012972019-11-23 02:23:11 +0000201 self.skip_servers = (options.skip_servers.split(','))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000202
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100203 def show_progress(self, how='.'):
204 if sys.stdout.isatty():
205 sys.stdout.write(how)
206 sys.stdout.flush()
207
Vadim Bendebury8de38002018-05-14 19:02:55 -0700208 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200209 # Convert the "key:value" filter to a list of (key, value) pairs.
210 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000211 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000212 # Instantiate the generator to force all the requests now and catch the
213 # errors here.
214 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000215 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
216 'CURRENT_REVISION', 'CURRENT_COMMIT']))
Raul Tambre7c938462019-05-24 16:35:35 +0000217 except gerrit_util.GerritError as e:
Vadim Bendebury8de38002018-05-14 19:02:55 -0700218 error_message = 'Looking up %r: %s' % (instance['url'], e)
219 if error_message not in self.access_errors:
220 self.access_errors.add(error_message)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000221 return []
222
deymo@chromium.org6c039202013-09-12 12:28:12 +0000223 def gerrit_search(self, instance, owner=None, reviewer=None):
Vadim Bendebury80012972019-11-23 02:23:11 +0000224 if instance['url'] in self.skip_servers:
225 return []
deymo@chromium.org6c039202013-09-12 12:28:12 +0000226 max_age = datetime.today() - self.modified_after
Andrii Shyshkalov4aedec02018-09-17 16:31:17 +0000227 filters = ['-age:%ss' % (max_age.days * 24 * 3600 + max_age.seconds)]
228 if owner:
229 assert not reviewer
230 filters.append('owner:%s' % owner)
231 else:
232 filters.extend(('-owner:%s' % reviewer, 'reviewer:%s' % reviewer))
Peter K. Lee9342ac02018-08-28 21:03:51 +0000233 # TODO(cjhopman): Should abandoned changes be filtered out when
234 # merged_only is not enabled?
235 if self.options.merged_only:
236 filters.append('status:merged')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000237
Aaron Gable2979a872017-09-05 17:38:32 -0700238 issues = self.gerrit_changes_over_rest(instance, filters)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100239 self.show_progress()
Aaron Gable2979a872017-09-05 17:38:32 -0700240 issues = [self.process_gerrit_issue(instance, issue)
241 for issue in issues]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000242
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000243 issues = filter(self.filter_issue, issues)
244 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
245
246 return issues
247
Aaron Gable2979a872017-09-05 17:38:32 -0700248 def process_gerrit_issue(self, instance, issue):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000249 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000250 if self.options.deltas:
251 ret['delta'] = DefaultFormatter().format(
252 '+{insertions},-{deletions}',
253 **issue)
254 ret['status'] = issue['status']
deymo@chromium.org6c039202013-09-12 12:28:12 +0000255 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700256 protocol = instance.get('short_url_protocol', 'http')
257 url = instance['shorturl']
258 else:
259 protocol = 'https'
260 url = instance['url']
261 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
262
deymo@chromium.org6c039202013-09-12 12:28:12 +0000263 ret['header'] = issue['subject']
Don Garrett2ebf9fd2018-08-06 15:14:00 +0000264 ret['owner'] = issue['owner'].get('email', '')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000265 ret['author'] = ret['owner']
266 ret['created'] = datetime_from_gerrit(issue['created'])
267 ret['modified'] = datetime_from_gerrit(issue['updated'])
268 if 'messages' in issue:
Aaron Gable2979a872017-09-05 17:38:32 -0700269 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
deymo@chromium.org6c039202013-09-12 12:28:12 +0000270 else:
271 ret['replies'] = []
272 ret['reviewers'] = set(r['author'] for r in ret['replies'])
273 ret['reviewers'].discard(ret['author'])
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000274 ret['bugs'] = extract_bug_numbers_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000275 return ret
276
277 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700278 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000279 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000280 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
281 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000282 for reply in replies:
283 ret.append({
284 'author': reply['author']['email'],
285 'created': datetime_from_gerrit(reply['date']),
286 'content': reply['message'],
287 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000288 return ret
289
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100290 def monorail_get_auth_http(self):
Kenneth Russell66badbd2018-09-09 21:35:32 +0000291 # Manually use a long timeout (10m); for some users who have a
292 # long history on the issue tracker, whatever the default timeout
293 # is is reached.
Edward Lemur5b929a42019-10-21 17:57:39 +0000294 return auth.Authenticator().authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100295
296 def filter_modified_monorail_issue(self, issue):
297 """Precisely checks if an issue has been modified in the time range.
298
299 This fetches all issue comments to check if the issue has been modified in
300 the time range specified by user. This is needed because monorail only
301 allows filtering by last updated and published dates, which is not
302 sufficient to tell whether a given issue has been modified at some specific
303 time range. Any update to the issue is a reported as comment on Monorail.
304
305 Args:
306 issue: Issue dict as returned by monorail_query_issues method. In
307 particular, must have a key 'uid' formatted as 'project:issue_id'.
308
309 Returns:
310 Passed issue if modified, None otherwise.
311 """
312 http = self.monorail_get_auth_http()
313 project, issue_id = issue['uid'].split(':')
314 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
315 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
316 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100317 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100318 content = json.loads(body)
319 if not content:
320 logging.error('Unable to parse %s response from monorail.', project)
321 return issue
322
323 for item in content.get('items', []):
324 comment_published = datetime_from_monorail(item['published'])
325 if self.filter_modified(comment_published):
326 return issue
327
328 return None
329
330 def monorail_query_issues(self, project, query):
331 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000332 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100333 '/%s/issues') % project
Edward Lemur2a048032020-01-14 22:58:13 +0000334 query_data = urllib_parse.urlencode(query)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100335 url = url + '?' + query_data
336 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100337 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100338 content = json.loads(body)
339 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100340 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100341 return []
342
343 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100344 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100345 for item in content.get('items', []):
346 if project_config.get('shorturl'):
347 protocol = project_config.get('short_url_protocol', 'http')
348 item_url = '%s://%s/%d' % (
349 protocol, project_config['shorturl'], item['id'])
350 else:
351 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
352 project, item['id'])
353 issue = {
354 'uid': '%s:%s' % (project, item['id']),
355 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100356 'created': datetime_from_monorail(item['published']),
357 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100358 'author': item['author']['name'],
359 'url': item_url,
360 'comments': [],
361 'status': item['status'],
362 'labels': [],
363 'components': []
364 }
365 if 'owner' in item:
366 issue['owner'] = item['owner']['name']
367 else:
368 issue['owner'] = 'None'
369 if 'labels' in item:
370 issue['labels'] = item['labels']
371 if 'components' in item:
372 issue['components'] = item['components']
373 issues.append(issue)
374
375 return issues
376
377 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000378 epoch = datetime.utcfromtimestamp(0)
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000379 # Defaults to @chromium.org email if one wasn't provided on -u option.
380 user_str = (self.options.email if self.options.email.find('@') >= 0
381 else '%s@chromium.org' % self.user)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000382
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100383 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000384 'maxResults': 10000,
385 'q': user_str,
386 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
387 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000388 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000389
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000390 if self.options.completed_issues:
391 return [
392 issue for issue in issues
393 if (self.match(issue['owner']) and
394 issue['status'].lower() in ('verified', 'fixed'))
395 ]
396
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100397 return [
398 issue for issue in issues
399 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000400
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100401 def monorail_get_issues(self, project, issue_ids):
402 return self.monorail_query_issues(project, {
403 'maxResults': 10000,
404 'q': 'id:%s' % ','.join(issue_ids)
405 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000406
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000407 def print_heading(self, heading):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000408 print()
409 print(self.options.output_format_heading.format(heading=heading))
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000410
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000411 def match(self, author):
412 if '@' in self.user:
413 return author == self.user
414 return author.startswith(self.user + '@')
415
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000416 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000417 activity = len([
418 reply
419 for reply in change['replies']
420 if self.match(reply['author'])
421 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000422 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000423 'created': change['created'].date().isoformat(),
424 'modified': change['modified'].date().isoformat(),
425 'reviewers': ', '.join(change['reviewers']),
426 'status': change['status'],
427 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000428 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000429 if self.options.deltas:
430 optional_values['delta'] = change['delta']
431
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000432 self.print_generic(self.options.output_format,
433 self.options.output_format_changes,
434 change['header'],
435 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000436 change['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000437 change['created'],
438 change['modified'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000439 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000440
441 def print_issue(self, issue):
442 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000443 'created': issue['created'].date().isoformat(),
444 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000445 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000446 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000447 }
448 self.print_generic(self.options.output_format,
449 self.options.output_format_issues,
450 issue['header'],
451 issue['url'],
452 issue['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000453 issue['created'],
454 issue['modified'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000455 optional_values)
456
457 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000458 activity = len([
459 reply
460 for reply in review['replies']
461 if self.match(reply['author'])
462 ])
463 optional_values = {
464 'created': review['created'].date().isoformat(),
465 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800466 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000467 'activity': activity,
468 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800469 if self.options.deltas:
470 optional_values['delta'] = review['delta']
471
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000472 self.print_generic(self.options.output_format,
473 self.options.output_format_reviews,
474 review['header'],
475 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000476 review['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000477 review['created'],
478 review['modified'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000479 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000480
481 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000482 def print_generic(default_fmt, specific_fmt,
Lutz Justen860378e2019-03-12 01:10:02 +0000483 title, url, author, created, modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000484 optional_values=None):
485 output_format = specific_fmt if specific_fmt is not None else default_fmt
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000486 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000487 'title': title,
488 'url': url,
489 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000490 'created': created,
491 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000492 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000493 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000494 values.update(optional_values)
Edward Lemur2a048032020-01-14 22:58:13 +0000495 print(DefaultFormatter().format(output_format, **values))
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000496
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000497
498 def filter_issue(self, issue, should_filter_by_user=True):
499 def maybe_filter_username(email):
500 return not should_filter_by_user or username(email) == self.user
501 if (maybe_filter_username(issue['author']) and
502 self.filter_modified(issue['created'])):
503 return True
504 if (maybe_filter_username(issue['owner']) and
505 (self.filter_modified(issue['created']) or
506 self.filter_modified(issue['modified']))):
507 return True
508 for reply in issue['replies']:
509 if self.filter_modified(reply['created']):
510 if not should_filter_by_user:
511 break
512 if (username(reply['author']) == self.user
513 or (self.user + '@') in reply['content']):
514 break
515 else:
516 return False
517 return True
518
519 def filter_modified(self, modified):
520 return self.modified_after < modified and modified < self.modified_before
521
522 def auth_for_changes(self):
523 #TODO(cjhopman): Move authentication check for getting changes here.
524 pass
525
526 def auth_for_reviews(self):
527 # Reviews use all the same instances as changes so no authentication is
528 # required.
529 pass
530
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000531 def get_changes(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000532 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100533 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100534 gerrit_changes = pool.map_async(
535 lambda instance: self.gerrit_search(instance, owner=self.user),
536 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100537 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000538 self.changes = list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000539
540 def print_changes(self):
541 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000542 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000543 for change in self.changes:
Bruce Dawson2afcf222019-02-25 22:07:08 +0000544 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000545
Vadim Bendebury8de38002018-05-14 19:02:55 -0700546 def print_access_errors(self):
547 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400548 logging.error('Access Errors:')
549 for error in self.access_errors:
550 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700551
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000552 def get_reviews(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000553 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100554 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100555 gerrit_reviews = pool.map_async(
556 lambda instance: self.gerrit_search(instance, reviewer=self.user),
557 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100558 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000559 self.reviews = list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000560
561 def print_reviews(self):
562 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000563 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000564 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000565 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000566
567 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100568 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
569 monorail_issues = pool.map(
570 self.monorail_issue_search, monorail_projects.keys())
571 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
572
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700573 if not monorail_issues:
574 return
575
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100576 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
577 filtered_issues = pool.map(
578 self.filter_modified_monorail_issue, monorail_issues)
579 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100580
581 def get_referenced_issues(self):
582 if not self.issues:
583 self.get_issues()
584
585 if not self.changes:
586 self.get_changes()
587
588 referenced_issue_uids = set(itertools.chain.from_iterable(
589 change['bugs'] for change in self.changes))
590 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
591 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
592
593 missing_issues_by_project = collections.defaultdict(list)
594 for issue_uid in missing_issue_uids:
595 project, issue_id = issue_uid.split(':')
596 missing_issues_by_project[project].append(issue_id)
597
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000598 for project, issue_ids in missing_issues_by_project.items():
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100599 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000600
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000601 def print_issues(self):
602 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000603 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000604 for issue in self.issues:
605 self.print_issue(issue)
606
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100607 def print_changes_by_issue(self, skip_empty_own):
608 if not self.issues or not self.changes:
609 return
610
611 self.print_heading('Changes by referenced issue(s)')
612 issues = {issue['uid']: issue for issue in self.issues}
613 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
614 changes_by_issue_uid = collections.defaultdict(list)
615 changes_by_ref_issue_uid = collections.defaultdict(list)
616 changes_without_issue = []
617 for change in self.changes:
618 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000619 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100620 if issue_uid in issues:
621 changes_by_issue_uid[issue_uid].append(change)
622 added = True
623 if issue_uid in ref_issues:
624 changes_by_ref_issue_uid[issue_uid].append(change)
625 added = True
626 if not added:
627 changes_without_issue.append(change)
628
629 # Changes referencing own issues.
630 for issue_uid in issues:
631 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
632 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000633 if changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000634 print()
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000635 for change in changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000636 print(' ', end='') # this prints no newline
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000637 self.print_change(change)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000638 print()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100639
640 # Changes referencing others' issues.
641 for issue_uid in ref_issues:
642 assert changes_by_ref_issue_uid[issue_uid]
643 self.print_issue(ref_issues[issue_uid])
644 for change in changes_by_ref_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000645 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100646 self.print_change(change)
647
648 # Changes referencing no issues.
649 if changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000650 print(self.options.output_format_no_url.format(title='Other changes'))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100651 for change in changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000652 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100653 self.print_change(change)
654
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000655 def print_activity(self):
656 self.print_changes()
657 self.print_reviews()
658 self.print_issues()
659
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000660 def dump_json(self, ignore_keys=None):
661 if ignore_keys is None:
662 ignore_keys = ['replies']
663
664 def format_for_json_dump(in_array):
665 output = {}
666 for item in in_array:
667 url = item.get('url') or item.get('review_url')
668 if not url:
669 raise Exception('Dumped item %s does not specify url' % item)
670 output[url] = dict(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000671 (k, v) for k,v in item.items() if k not in ignore_keys)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000672 return output
673
674 class PythonObjectEncoder(json.JSONEncoder):
675 def default(self, obj): # pylint: disable=method-hidden
676 if isinstance(obj, datetime):
677 return obj.isoformat()
678 if isinstance(obj, set):
679 return list(obj)
680 return json.JSONEncoder.default(self, obj)
681
682 output = {
683 'reviews': format_for_json_dump(self.reviews),
684 'changes': format_for_json_dump(self.changes),
685 'issues': format_for_json_dump(self.issues)
686 }
Raul Tambre80ee78e2019-05-06 22:41:05 +0000687 print(json.dumps(output, indent=2, cls=PythonObjectEncoder))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000688
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000689
690def main():
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000691 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
692 parser.add_option(
693 '-u', '--user', metavar='<email>',
Bruce Dawson84370962019-02-21 05:33:37 +0000694 # Look for USER and USERNAME (Windows) environment variables.
695 default=os.environ.get('USER', os.environ.get('USERNAME')),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000696 help='Filter on user, default=%default')
697 parser.add_option(
698 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000699 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000700 parser.add_option(
701 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000702 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000703 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
704 relativedelta(months=2))
705 parser.add_option(
706 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000707 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000708 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
709 parser.add_option(
710 '-Y', '--this_year', action='store_true',
711 help='Use this year\'s dates')
712 parser.add_option(
713 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000714 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000715 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000716 '-W', '--last_week', action='count',
717 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000718 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000719 '-a', '--auth',
720 action='store_true',
721 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000722 parser.add_option(
723 '-d', '--deltas',
724 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800725 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100726 parser.add_option(
727 '--no-referenced-issues',
728 action='store_true',
729 help='Do not fetch issues referenced by owned changes. Useful in '
730 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100731 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100732 parser.add_option(
Vadim Bendebury80012972019-11-23 02:23:11 +0000733 '--skip_servers',
734 action='store',
735 default='',
736 help='A comma separated list of gerrit and rietveld servers to ignore')
737 parser.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100738 '--skip-own-issues-without-changes',
739 action='store_true',
740 help='Skips listing own issues without changes when showing changes '
741 'grouped by referenced issue(s). See --changes-by-issue for more '
742 'details.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000743
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000744 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000745 'By default, all activity will be looked up and '
746 'printed. If any of these are specified, only '
747 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000748 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000749 '-c', '--changes',
750 action='store_true',
751 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000752 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000753 '-i', '--issues',
754 action='store_true',
755 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000756 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000757 '-r', '--reviews',
758 action='store_true',
759 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100760 activity_types_group.add_option(
761 '--changes-by-issue', action='store_true',
762 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000763 parser.add_option_group(activity_types_group)
764
765 output_format_group = optparse.OptionGroup(parser, 'Output Format',
766 'By default, all activity will be printed in the '
767 'following format: {url} {title}. This can be '
768 'changed for either all activity types or '
769 'individually for each activity type. The format '
770 'is defined as documented for '
771 'string.format(...). The variables available for '
Lutz Justen860378e2019-03-12 01:10:02 +0000772 'all activity types are url, title, author, '
773 'created and modified. Format options for '
774 'specific activity types will override the '
775 'generic format.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000776 output_format_group.add_option(
777 '-f', '--output-format', metavar='<format>',
778 default=u'{url} {title}',
779 help='Specifies the format to use when printing all your activity.')
780 output_format_group.add_option(
781 '--output-format-changes', metavar='<format>',
782 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000783 help='Specifies the format to use when printing changes. Supports the '
784 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000785 output_format_group.add_option(
786 '--output-format-issues', metavar='<format>',
787 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000788 help='Specifies the format to use when printing issues. Supports the '
789 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000790 output_format_group.add_option(
791 '--output-format-reviews', metavar='<format>',
792 default=None,
793 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000794 output_format_group.add_option(
795 '--output-format-heading', metavar='<format>',
796 default=u'{heading}:',
797 help='Specifies the format to use when printing headings.')
798 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100799 '--output-format-no-url', default='{title}',
800 help='Specifies the format to use when printing activity without url.')
801 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000802 '-m', '--markdown', action='store_true',
803 help='Use markdown-friendly output (overrides --output-format '
804 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000805 output_format_group.add_option(
806 '-j', '--json', action='store_true',
807 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000808 parser.add_option_group(output_format_group)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000809
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000810 parser.add_option(
811 '-v', '--verbose',
812 action='store_const',
813 dest='verbosity',
814 default=logging.WARN,
815 const=logging.INFO,
816 help='Output extra informational messages.'
817 )
818 parser.add_option(
819 '-q', '--quiet',
820 action='store_const',
821 dest='verbosity',
822 const=logging.ERROR,
823 help='Suppress non-error messages.'
824 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000825 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000826 '-M', '--merged-only',
827 action='store_true',
828 dest='merged_only',
829 default=False,
830 help='Shows only changes that have been merged.')
831 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000832 '-C', '--completed-issues',
833 action='store_true',
834 dest='completed_issues',
835 default=False,
836 help='Shows only monorail issues that have completed (Fixed|Verified) '
837 'by the user.')
838 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000839 '-o', '--output', metavar='<file>',
840 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000841
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000842 # Remove description formatting
843 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800844 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000845
846 options, args = parser.parse_args()
847 options.local_user = os.environ.get('USER')
848 if args:
849 parser.error('Args unsupported')
850 if not options.user:
Bruce Dawson84370962019-02-21 05:33:37 +0000851 parser.error('USER/USERNAME is not set, please use -u')
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000852 # Retains the original -u option as the email address.
853 options.email = options.user
854 options.user = username(options.email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000855
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000856 logging.basicConfig(level=options.verbosity)
857
858 # python-keyring provides easy access to the system keyring.
859 try:
860 import keyring # pylint: disable=unused-import,unused-variable,F0401
861 except ImportError:
862 logging.warning('Consider installing python-keyring')
863
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000864 if not options.begin:
865 if options.last_quarter:
866 begin, end = quarter_begin, quarter_end
867 elif options.this_year:
868 begin, end = get_year_of(datetime.today())
869 elif options.week_of:
870 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000871 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000872 begin, end = (get_week_of(datetime.today() -
873 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000874 else:
875 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
876 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700877 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000878 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700879 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000880 else:
881 end = datetime.today()
882 options.begin, options.end = begin, end
Bruce Dawson2afcf222019-02-25 22:07:08 +0000883 if begin >= end:
884 # The queries fail in peculiar ways when the begin date is in the future.
885 # Give a descriptive error message instead.
886 logging.error('Start date (%s) is the same or later than end date (%s)' %
887 (begin, end))
888 return 1
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000889
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000890 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000891 options.output_format_heading = '### {heading}\n'
892 options.output_format = ' * [{title}]({url})'
893 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000894 logging.info('Searching for activity by %s', options.user)
895 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000896
897 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100898 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000899
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100900 if not (options.changes or options.reviews or options.issues or
901 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000902 options.changes = True
903 options.issues = True
904 options.reviews = True
905
906 # First do any required authentication so none of the user interaction has to
907 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100908 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000909 my_activity.auth_for_changes()
910 if options.reviews:
911 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000912
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000913 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000914
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000915 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100916 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000917 my_activity.get_changes()
918 if options.reviews:
919 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100920 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000921 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100922 if not options.no_referenced_issues:
923 my_activity.get_referenced_issues()
Edward Lemur5b929a42019-10-21 17:57:39 +0000924 except auth.LoginRequiredError as e:
925 logging.error('auth.LoginRequiredError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000926
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100927 my_activity.show_progress('\n')
928
Vadim Bendebury8de38002018-05-14 19:02:55 -0700929 my_activity.print_access_errors()
930
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000931 output_file = None
932 try:
933 if options.output:
934 output_file = open(options.output, 'w')
935 logging.info('Printing output to "%s"', options.output)
936 sys.stdout = output_file
937 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700938 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000939 else:
940 if options.json:
941 my_activity.dump_json()
942 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100943 if options.changes:
944 my_activity.print_changes()
945 if options.reviews:
946 my_activity.print_reviews()
947 if options.issues:
948 my_activity.print_issues()
949 if options.changes_by_issue:
950 my_activity.print_changes_by_issue(
951 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000952 finally:
953 if output_file:
954 logging.info('Done printing to file.')
955 sys.stdout = sys.__stdout__
956 output_file.close()
957
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000958 return 0
959
960
961if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000962 # Fix encoding to support non-ascii issue titles.
963 fix_encoding.fix_encoding()
964
sbc@chromium.org013731e2015-02-26 18:28:43 +0000965 try:
966 sys.exit(main())
967 except KeyboardInterrupt:
968 sys.stderr.write('interrupted\n')
969 sys.exit(1)