blob: 874f9f3860a47e598f80e9fca48a8e41c57e8317 [file] [log] [blame]
Edward Lemura3b6fd02020-03-02 22:16:15 +00001#!/usr/bin/env vpython3
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Get stats about your activity.
7
8Example:
9 - my_activity.py for stats for the current week (last week on mondays).
10 - my_activity.py -Q for stats for last quarter.
11 - my_activity.py -Y for stats for this year.
Mohamed Heikaldc37feb2019-06-28 22:33:58 +000012 - my_activity.py -b 4/24/19 for stats since April 24th 2019.
13 - my_activity.py -b 4/24/19 -e 6/16/19 stats between April 24th and June 16th.
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +000014 - my_activity.py -jd to output stats for the week to json with deltas data.
Nicolas Boichatcd3696c2021-06-02 01:42:18 +000015
16To add additional gerrit instances, one can pass a JSON file as parameter:
17 - my_activity.py -F config.json
18{
19 "gerrit_instances": {
20 "team-internal-review.googlesource.com": {
21 "shorturl": "go/teamcl",
22 "short_url_protocol": "http"
23 },
24 "team-external-review.googlesource.com": {}
25 }
26}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000027"""
28
29# These services typically only provide a created time and a last modified time
30# for each item for general queries. This is not enough to determine if there
31# was activity in a given time period. So, we first query for all things created
32# before end and modified after begin. Then, we get the details of each item and
33# check those details to determine if there was activity in the given period.
34# This means that query time scales mostly with (today() - begin).
35
Raul Tambre80ee78e2019-05-06 22:41:05 +000036from __future__ import print_function
37
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010038import collections
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +010039import contextlib
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000040from datetime import datetime
41from datetime import timedelta
Edward Lemur202c5592019-10-21 22:44:52 +000042import httplib2
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010043import itertools
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000044import json
Tobias Sargeantffb3c432017-03-08 14:09:14 +000045import logging
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +010046from multiprocessing.pool import ThreadPool
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000047import optparse
48import os
49import subprocess
Tobias Sargeantffb3c432017-03-08 14:09:14 +000050from string import Formatter
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000051import sys
52import urllib
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +000053import re
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000054
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000055import auth
stevefung@chromium.org832d51e2015-05-27 12:52:51 +000056import fix_encoding
Edward Lesmesae3586b2020-03-23 21:21:14 +000057import gclient_utils
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000058import gerrit_util
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000059
Edward Lemur2a048032020-01-14 22:58:13 +000060if sys.version_info.major == 2:
Josip Sokcevic4940cc42021-10-05 23:55:34 +000061 logging.critical(
62 'Python 2 is not supported. Run my_activity.py using vpython3.')
63
Edward Lemur2a048032020-01-14 22:58:13 +000064
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000065try:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000066 import dateutil # pylint: disable=import-error
67 import dateutil.parser
68 from dateutil.relativedelta import relativedelta
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000069except ImportError:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000070 logging.error('python-dateutil package required')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000071 exit(1)
72
Tobias Sargeantffb3c432017-03-08 14:09:14 +000073
74class DefaultFormatter(Formatter):
75 def __init__(self, default = ''):
76 super(DefaultFormatter, self).__init__()
77 self.default = default
78
79 def get_value(self, key, args, kwds):
Edward Lemur2a048032020-01-14 22:58:13 +000080 if isinstance(key, str) and key not in kwds:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000081 return self.default
82 return Formatter.get_value(self, key, args, kwds)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000083
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000084gerrit_instances = [
85 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000086 'url': 'android-review.googlesource.com',
Mike Frysingerfdf027a2020-02-27 21:53:45 +000087 'shorturl': 'r.android.com',
88 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000089 },
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000090 {
Mike Frysinger24fd8632020-02-27 22:07:06 +000091 'url': 'gerrit-review.googlesource.com',
92 },
93 {
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000094 'url': 'chrome-internal-review.googlesource.com',
Jeremy Romanf475a472017-06-06 16:49:11 -040095 'shorturl': 'crrev.com/i',
Varun Khanejad9f97bc2017-08-02 12:55:01 -070096 'short_url_protocol': 'https',
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000097 },
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000098 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000099 'url': 'chromium-review.googlesource.com',
100 'shorturl': 'crrev.com/c',
101 'short_url_protocol': 'https',
deymo@chromium.org56dc57a2015-09-10 18:26:54 +0000102 },
Ryan Harrison897602a2017-09-18 16:23:41 -0400103 {
Ryan Harrison06e18692019-09-23 18:22:25 +0000104 'url': 'dawn-review.googlesource.com',
105 },
106 {
Ryan Harrison897602a2017-09-18 16:23:41 -0400107 'url': 'pdfium-review.googlesource.com',
108 },
Adrienne Walker95d4c852018-09-27 20:28:12 +0000109 {
110 'url': 'skia-review.googlesource.com',
111 },
Paul Fagerburgb93d82c2020-08-17 16:19:46 +0000112 {
113 'url': 'review.coreboot.org',
114 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000115]
116
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100117monorail_projects = {
Jamie Madill1db68ea2019-09-03 19:34:42 +0000118 'angleproject': {
119 'shorturl': 'anglebug.com',
120 'short_url_protocol': 'http',
121 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100122 'chromium': {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000123 'shorturl': 'crbug.com',
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700124 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000125 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000126 'dawn': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100127 'google-breakpad': {},
128 'gyp': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100129 'pdfium': {
Ryan Harrison897602a2017-09-18 16:23:41 -0400130 'shorturl': 'crbug.com/pdfium',
131 'short_url_protocol': 'https',
132 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000133 'skia': {},
Ryan Harrison97811152021-03-29 20:30:57 +0000134 'tint': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100135 'v8': {
136 'shorturl': 'crbug.com/v8',
137 'short_url_protocol': 'https',
138 },
139}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000140
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000141def username(email):
142 """Keeps the username of an email address."""
143 return email and email.split('@', 1)[0]
144
145
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000146def datetime_to_midnight(date):
147 return date - timedelta(hours=date.hour, minutes=date.minute,
148 seconds=date.second, microseconds=date.microsecond)
149
150
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000151def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000152 begin = (datetime_to_midnight(date) -
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000153 relativedelta(months=(date.month - 1) % 3, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000154 return begin, begin + relativedelta(months=3)
155
156
157def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000158 begin = (datetime_to_midnight(date) -
159 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000160 return begin, begin + relativedelta(years=1)
161
162
163def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000164 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000165 return begin, begin + timedelta(days=7)
166
167
168def get_yes_or_no(msg):
169 while True:
Edward Lesmesae3586b2020-03-23 21:21:14 +0000170 response = gclient_utils.AskForData(msg + ' yes/no [no] ')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000171 if response == 'y' or response == 'yes':
172 return True
173 elif not response or response == 'n' or response == 'no':
174 return False
175
176
deymo@chromium.org6c039202013-09-12 12:28:12 +0000177def datetime_from_gerrit(date_string):
178 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
179
180
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100181def datetime_from_monorail(date_string):
182 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000183
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000184def extract_bug_numbers_from_description(issue):
185 # Getting the description for REST Gerrit
186 revision = issue['revisions'][issue['current_revision']]
187 description = revision['commit']['message']
188
189 bugs = []
190 # Handle both "Bug: 99999" and "BUG=99999" bug notations
191 # Multiple bugs can be noted on a single line or in multiple ones.
192 matches = re.findall(
193 r'^(BUG=|(Bug|Fixed):\s*)((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)',
194 description, flags=re.IGNORECASE | re.MULTILINE)
195 if matches:
196 for match in matches:
197 bugs.extend(match[2].replace(' ', '').split(','))
198 # Add default chromium: prefix if none specified.
199 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
200
201 return sorted(set(bugs))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000202
203class MyActivity(object):
204 def __init__(self, options):
205 self.options = options
206 self.modified_after = options.begin
207 self.modified_before = options.end
208 self.user = options.user
209 self.changes = []
210 self.reviews = []
211 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100212 self.referenced_issues = []
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000213 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-14 19:02:55 -0700214 self.access_errors = set()
Vadim Bendebury80012972019-11-23 02:23:11 +0000215 self.skip_servers = (options.skip_servers.split(','))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000216
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100217 def show_progress(self, how='.'):
218 if sys.stdout.isatty():
219 sys.stdout.write(how)
220 sys.stdout.flush()
221
Vadim Bendebury8de38002018-05-14 19:02:55 -0700222 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200223 # Convert the "key:value" filter to a list of (key, value) pairs.
224 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000225 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000226 # Instantiate the generator to force all the requests now and catch the
227 # errors here.
228 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000229 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
230 'CURRENT_REVISION', 'CURRENT_COMMIT']))
Raul Tambre7c938462019-05-24 16:35:35 +0000231 except gerrit_util.GerritError as e:
Vadim Bendebury8de38002018-05-14 19:02:55 -0700232 error_message = 'Looking up %r: %s' % (instance['url'], e)
233 if error_message not in self.access_errors:
234 self.access_errors.add(error_message)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000235 return []
236
deymo@chromium.org6c039202013-09-12 12:28:12 +0000237 def gerrit_search(self, instance, owner=None, reviewer=None):
Vadim Bendebury80012972019-11-23 02:23:11 +0000238 if instance['url'] in self.skip_servers:
239 return []
deymo@chromium.org6c039202013-09-12 12:28:12 +0000240 max_age = datetime.today() - self.modified_after
Andrii Shyshkalov4aedec02018-09-17 16:31:17 +0000241 filters = ['-age:%ss' % (max_age.days * 24 * 3600 + max_age.seconds)]
242 if owner:
243 assert not reviewer
244 filters.append('owner:%s' % owner)
245 else:
246 filters.extend(('-owner:%s' % reviewer, 'reviewer:%s' % reviewer))
Peter K. Lee9342ac02018-08-28 21:03:51 +0000247 # TODO(cjhopman): Should abandoned changes be filtered out when
248 # merged_only is not enabled?
249 if self.options.merged_only:
250 filters.append('status:merged')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000251
Aaron Gable2979a872017-09-05 17:38:32 -0700252 issues = self.gerrit_changes_over_rest(instance, filters)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100253 self.show_progress()
Aaron Gable2979a872017-09-05 17:38:32 -0700254 issues = [self.process_gerrit_issue(instance, issue)
255 for issue in issues]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000256
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000257 issues = filter(self.filter_issue, issues)
258 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
259
260 return issues
261
Aaron Gable2979a872017-09-05 17:38:32 -0700262 def process_gerrit_issue(self, instance, issue):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000263 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000264 if self.options.deltas:
265 ret['delta'] = DefaultFormatter().format(
266 '+{insertions},-{deletions}',
267 **issue)
268 ret['status'] = issue['status']
deymo@chromium.org6c039202013-09-12 12:28:12 +0000269 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700270 protocol = instance.get('short_url_protocol', 'http')
271 url = instance['shorturl']
272 else:
273 protocol = 'https'
274 url = instance['url']
275 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
276
deymo@chromium.org6c039202013-09-12 12:28:12 +0000277 ret['header'] = issue['subject']
Don Garrett2ebf9fd2018-08-06 15:14:00 +0000278 ret['owner'] = issue['owner'].get('email', '')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000279 ret['author'] = ret['owner']
280 ret['created'] = datetime_from_gerrit(issue['created'])
281 ret['modified'] = datetime_from_gerrit(issue['updated'])
282 if 'messages' in issue:
Aaron Gable2979a872017-09-05 17:38:32 -0700283 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
deymo@chromium.org6c039202013-09-12 12:28:12 +0000284 else:
285 ret['replies'] = []
286 ret['reviewers'] = set(r['author'] for r in ret['replies'])
287 ret['reviewers'].discard(ret['author'])
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000288 ret['bugs'] = extract_bug_numbers_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000289 return ret
290
291 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700292 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000293 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000294 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
295 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000296 for reply in replies:
297 ret.append({
298 'author': reply['author']['email'],
299 'created': datetime_from_gerrit(reply['date']),
300 'content': reply['message'],
301 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000302 return ret
303
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100304 def monorail_get_auth_http(self):
Kenneth Russell66badbd2018-09-09 21:35:32 +0000305 # Manually use a long timeout (10m); for some users who have a
306 # long history on the issue tracker, whatever the default timeout
307 # is is reached.
Edward Lemur5b929a42019-10-21 17:57:39 +0000308 return auth.Authenticator().authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100309
310 def filter_modified_monorail_issue(self, issue):
311 """Precisely checks if an issue has been modified in the time range.
312
313 This fetches all issue comments to check if the issue has been modified in
314 the time range specified by user. This is needed because monorail only
315 allows filtering by last updated and published dates, which is not
316 sufficient to tell whether a given issue has been modified at some specific
317 time range. Any update to the issue is a reported as comment on Monorail.
318
319 Args:
320 issue: Issue dict as returned by monorail_query_issues method. In
321 particular, must have a key 'uid' formatted as 'project:issue_id'.
322
323 Returns:
324 Passed issue if modified, None otherwise.
325 """
326 http = self.monorail_get_auth_http()
327 project, issue_id = issue['uid'].split(':')
328 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
329 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
330 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100331 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100332 content = json.loads(body)
333 if not content:
334 logging.error('Unable to parse %s response from monorail.', project)
335 return issue
336
337 for item in content.get('items', []):
338 comment_published = datetime_from_monorail(item['published'])
339 if self.filter_modified(comment_published):
340 return issue
341
342 return None
343
344 def monorail_query_issues(self, project, query):
345 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000346 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100347 '/%s/issues') % project
Josip Sokcevic4940cc42021-10-05 23:55:34 +0000348 query_data = urllib.parse.urlencode(query)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100349 url = url + '?' + query_data
350 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100351 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100352 content = json.loads(body)
353 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100354 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100355 return []
356
357 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100358 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100359 for item in content.get('items', []):
360 if project_config.get('shorturl'):
361 protocol = project_config.get('short_url_protocol', 'http')
362 item_url = '%s://%s/%d' % (
363 protocol, project_config['shorturl'], item['id'])
364 else:
365 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
366 project, item['id'])
367 issue = {
368 'uid': '%s:%s' % (project, item['id']),
369 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100370 'created': datetime_from_monorail(item['published']),
371 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100372 'author': item['author']['name'],
373 'url': item_url,
374 'comments': [],
375 'status': item['status'],
376 'labels': [],
377 'components': []
378 }
379 if 'owner' in item:
380 issue['owner'] = item['owner']['name']
381 else:
382 issue['owner'] = 'None'
383 if 'labels' in item:
384 issue['labels'] = item['labels']
385 if 'components' in item:
386 issue['components'] = item['components']
387 issues.append(issue)
388
389 return issues
390
391 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000392 epoch = datetime.utcfromtimestamp(0)
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000393 # Defaults to @chromium.org email if one wasn't provided on -u option.
394 user_str = (self.options.email if self.options.email.find('@') >= 0
395 else '%s@chromium.org' % self.user)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000396
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100397 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000398 'maxResults': 10000,
399 'q': user_str,
400 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
401 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000402 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000403
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000404 if self.options.completed_issues:
405 return [
406 issue for issue in issues
407 if (self.match(issue['owner']) and
408 issue['status'].lower() in ('verified', 'fixed'))
409 ]
410
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100411 return [
412 issue for issue in issues
413 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000414
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100415 def monorail_get_issues(self, project, issue_ids):
416 return self.monorail_query_issues(project, {
417 'maxResults': 10000,
418 'q': 'id:%s' % ','.join(issue_ids)
419 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000420
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000421 def print_heading(self, heading):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000422 print()
423 print(self.options.output_format_heading.format(heading=heading))
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000424
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000425 def match(self, author):
426 if '@' in self.user:
427 return author == self.user
428 return author.startswith(self.user + '@')
429
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000430 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000431 activity = len([
432 reply
433 for reply in change['replies']
434 if self.match(reply['author'])
435 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000436 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000437 'created': change['created'].date().isoformat(),
438 'modified': change['modified'].date().isoformat(),
439 'reviewers': ', '.join(change['reviewers']),
440 'status': change['status'],
441 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000442 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000443 if self.options.deltas:
444 optional_values['delta'] = change['delta']
445
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000446 self.print_generic(self.options.output_format,
447 self.options.output_format_changes,
448 change['header'],
449 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000450 change['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000451 change['created'],
452 change['modified'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000453 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000454
455 def print_issue(self, issue):
456 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000457 'created': issue['created'].date().isoformat(),
458 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000459 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000460 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000461 }
462 self.print_generic(self.options.output_format,
463 self.options.output_format_issues,
464 issue['header'],
465 issue['url'],
466 issue['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000467 issue['created'],
468 issue['modified'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000469 optional_values)
470
471 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000472 activity = len([
473 reply
474 for reply in review['replies']
475 if self.match(reply['author'])
476 ])
477 optional_values = {
478 'created': review['created'].date().isoformat(),
479 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800480 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000481 'activity': activity,
482 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800483 if self.options.deltas:
484 optional_values['delta'] = review['delta']
485
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000486 self.print_generic(self.options.output_format,
487 self.options.output_format_reviews,
488 review['header'],
489 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000490 review['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000491 review['created'],
492 review['modified'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000493 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000494
495 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000496 def print_generic(default_fmt, specific_fmt,
Lutz Justen860378e2019-03-12 01:10:02 +0000497 title, url, author, created, modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000498 optional_values=None):
499 output_format = specific_fmt if specific_fmt is not None else default_fmt
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000500 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000501 'title': title,
502 'url': url,
503 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000504 'created': created,
505 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000506 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000507 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000508 values.update(optional_values)
Edward Lemur2a048032020-01-14 22:58:13 +0000509 print(DefaultFormatter().format(output_format, **values))
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000510
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000511
512 def filter_issue(self, issue, should_filter_by_user=True):
513 def maybe_filter_username(email):
514 return not should_filter_by_user or username(email) == self.user
515 if (maybe_filter_username(issue['author']) and
516 self.filter_modified(issue['created'])):
517 return True
518 if (maybe_filter_username(issue['owner']) and
519 (self.filter_modified(issue['created']) or
520 self.filter_modified(issue['modified']))):
521 return True
522 for reply in issue['replies']:
523 if self.filter_modified(reply['created']):
524 if not should_filter_by_user:
525 break
526 if (username(reply['author']) == self.user
527 or (self.user + '@') in reply['content']):
528 break
529 else:
530 return False
531 return True
532
533 def filter_modified(self, modified):
534 return self.modified_after < modified and modified < self.modified_before
535
536 def auth_for_changes(self):
537 #TODO(cjhopman): Move authentication check for getting changes here.
538 pass
539
540 def auth_for_reviews(self):
541 # Reviews use all the same instances as changes so no authentication is
542 # required.
543 pass
544
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000545 def get_changes(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000546 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100547 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100548 gerrit_changes = pool.map_async(
549 lambda instance: self.gerrit_search(instance, owner=self.user),
550 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100551 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000552 self.changes = list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000553
554 def print_changes(self):
555 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000556 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000557 for change in self.changes:
Bruce Dawson2afcf222019-02-25 22:07:08 +0000558 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000559
Vadim Bendebury8de38002018-05-14 19:02:55 -0700560 def print_access_errors(self):
561 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400562 logging.error('Access Errors:')
563 for error in self.access_errors:
564 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700565
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000566 def get_reviews(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000567 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100568 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100569 gerrit_reviews = pool.map_async(
570 lambda instance: self.gerrit_search(instance, reviewer=self.user),
571 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100572 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000573 self.reviews = list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000574
575 def print_reviews(self):
576 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000577 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000578 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000579 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000580
581 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100582 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
583 monorail_issues = pool.map(
584 self.monorail_issue_search, monorail_projects.keys())
585 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
586
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700587 if not monorail_issues:
588 return
589
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100590 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
591 filtered_issues = pool.map(
592 self.filter_modified_monorail_issue, monorail_issues)
593 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100594
595 def get_referenced_issues(self):
596 if not self.issues:
597 self.get_issues()
598
599 if not self.changes:
600 self.get_changes()
601
602 referenced_issue_uids = set(itertools.chain.from_iterable(
603 change['bugs'] for change in self.changes))
604 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
605 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
606
607 missing_issues_by_project = collections.defaultdict(list)
608 for issue_uid in missing_issue_uids:
609 project, issue_id = issue_uid.split(':')
610 missing_issues_by_project[project].append(issue_id)
611
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000612 for project, issue_ids in missing_issues_by_project.items():
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100613 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000614
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000615 def print_issues(self):
616 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000617 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000618 for issue in self.issues:
619 self.print_issue(issue)
620
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100621 def print_changes_by_issue(self, skip_empty_own):
622 if not self.issues or not self.changes:
623 return
624
625 self.print_heading('Changes by referenced issue(s)')
626 issues = {issue['uid']: issue for issue in self.issues}
627 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
628 changes_by_issue_uid = collections.defaultdict(list)
629 changes_by_ref_issue_uid = collections.defaultdict(list)
630 changes_without_issue = []
631 for change in self.changes:
632 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000633 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100634 if issue_uid in issues:
635 changes_by_issue_uid[issue_uid].append(change)
636 added = True
637 if issue_uid in ref_issues:
638 changes_by_ref_issue_uid[issue_uid].append(change)
639 added = True
640 if not added:
641 changes_without_issue.append(change)
642
643 # Changes referencing own issues.
644 for issue_uid in issues:
645 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
646 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000647 if changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000648 print()
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000649 for change in changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000650 print(' ', end='') # this prints no newline
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000651 self.print_change(change)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000652 print()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100653
654 # Changes referencing others' issues.
655 for issue_uid in ref_issues:
656 assert changes_by_ref_issue_uid[issue_uid]
657 self.print_issue(ref_issues[issue_uid])
658 for change in changes_by_ref_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000659 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100660 self.print_change(change)
661
662 # Changes referencing no issues.
663 if changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000664 print(self.options.output_format_no_url.format(title='Other changes'))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100665 for change in changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000666 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100667 self.print_change(change)
668
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000669 def print_activity(self):
670 self.print_changes()
671 self.print_reviews()
672 self.print_issues()
673
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000674 def dump_json(self, ignore_keys=None):
675 if ignore_keys is None:
676 ignore_keys = ['replies']
677
678 def format_for_json_dump(in_array):
679 output = {}
680 for item in in_array:
681 url = item.get('url') or item.get('review_url')
682 if not url:
683 raise Exception('Dumped item %s does not specify url' % item)
684 output[url] = dict(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000685 (k, v) for k,v in item.items() if k not in ignore_keys)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000686 return output
687
688 class PythonObjectEncoder(json.JSONEncoder):
689 def default(self, obj): # pylint: disable=method-hidden
690 if isinstance(obj, datetime):
691 return obj.isoformat()
692 if isinstance(obj, set):
693 return list(obj)
694 return json.JSONEncoder.default(self, obj)
695
696 output = {
697 'reviews': format_for_json_dump(self.reviews),
698 'changes': format_for_json_dump(self.changes),
699 'issues': format_for_json_dump(self.issues)
700 }
Raul Tambre80ee78e2019-05-06 22:41:05 +0000701 print(json.dumps(output, indent=2, cls=PythonObjectEncoder))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000702
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000703
704def main():
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000705 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
706 parser.add_option(
707 '-u', '--user', metavar='<email>',
Bruce Dawson84370962019-02-21 05:33:37 +0000708 # Look for USER and USERNAME (Windows) environment variables.
709 default=os.environ.get('USER', os.environ.get('USERNAME')),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000710 help='Filter on user, default=%default')
711 parser.add_option(
712 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000713 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000714 parser.add_option(
715 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000716 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000717 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
718 relativedelta(months=2))
719 parser.add_option(
720 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000721 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000722 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
723 parser.add_option(
724 '-Y', '--this_year', action='store_true',
725 help='Use this year\'s dates')
726 parser.add_option(
727 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000728 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000729 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000730 '-W', '--last_week', action='count',
731 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000732 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000733 '-a', '--auth',
734 action='store_true',
735 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000736 parser.add_option(
737 '-d', '--deltas',
738 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800739 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100740 parser.add_option(
741 '--no-referenced-issues',
742 action='store_true',
743 help='Do not fetch issues referenced by owned changes. Useful in '
744 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100745 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100746 parser.add_option(
Vadim Bendebury80012972019-11-23 02:23:11 +0000747 '--skip_servers',
748 action='store',
749 default='',
750 help='A comma separated list of gerrit and rietveld servers to ignore')
751 parser.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100752 '--skip-own-issues-without-changes',
753 action='store_true',
754 help='Skips listing own issues without changes when showing changes '
755 'grouped by referenced issue(s). See --changes-by-issue for more '
756 'details.')
Nicolas Boichatcd3696c2021-06-02 01:42:18 +0000757 parser.add_option(
758 '-F', '--config_file', metavar='<config_file>',
759 help='Configuration file in JSON format, used to add additional gerrit '
760 'instances (see source code for an example).')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000761
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000762 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000763 'By default, all activity will be looked up and '
764 'printed. If any of these are specified, only '
765 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000766 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000767 '-c', '--changes',
768 action='store_true',
769 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000770 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000771 '-i', '--issues',
772 action='store_true',
773 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000774 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000775 '-r', '--reviews',
776 action='store_true',
777 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100778 activity_types_group.add_option(
779 '--changes-by-issue', action='store_true',
780 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000781 parser.add_option_group(activity_types_group)
782
783 output_format_group = optparse.OptionGroup(parser, 'Output Format',
784 'By default, all activity will be printed in the '
785 'following format: {url} {title}. This can be '
786 'changed for either all activity types or '
787 'individually for each activity type. The format '
788 'is defined as documented for '
789 'string.format(...). The variables available for '
Lutz Justen860378e2019-03-12 01:10:02 +0000790 'all activity types are url, title, author, '
791 'created and modified. Format options for '
792 'specific activity types will override the '
793 'generic format.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000794 output_format_group.add_option(
795 '-f', '--output-format', metavar='<format>',
796 default=u'{url} {title}',
797 help='Specifies the format to use when printing all your activity.')
798 output_format_group.add_option(
799 '--output-format-changes', metavar='<format>',
800 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000801 help='Specifies the format to use when printing changes. Supports the '
802 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000803 output_format_group.add_option(
804 '--output-format-issues', metavar='<format>',
805 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000806 help='Specifies the format to use when printing issues. Supports the '
807 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000808 output_format_group.add_option(
809 '--output-format-reviews', metavar='<format>',
810 default=None,
811 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000812 output_format_group.add_option(
813 '--output-format-heading', metavar='<format>',
814 default=u'{heading}:',
Vincent Scheib2be61a12020-05-26 16:45:32 +0000815 help='Specifies the format to use when printing headings. '
816 'Supports the variable {heading}.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000817 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100818 '--output-format-no-url', default='{title}',
819 help='Specifies the format to use when printing activity without url.')
820 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000821 '-m', '--markdown', action='store_true',
822 help='Use markdown-friendly output (overrides --output-format '
823 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000824 output_format_group.add_option(
825 '-j', '--json', action='store_true',
826 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000827 parser.add_option_group(output_format_group)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000828
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000829 parser.add_option(
830 '-v', '--verbose',
831 action='store_const',
832 dest='verbosity',
833 default=logging.WARN,
834 const=logging.INFO,
835 help='Output extra informational messages.'
836 )
837 parser.add_option(
838 '-q', '--quiet',
839 action='store_const',
840 dest='verbosity',
841 const=logging.ERROR,
842 help='Suppress non-error messages.'
843 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000844 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000845 '-M', '--merged-only',
846 action='store_true',
847 dest='merged_only',
848 default=False,
849 help='Shows only changes that have been merged.')
850 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000851 '-C', '--completed-issues',
852 action='store_true',
853 dest='completed_issues',
854 default=False,
855 help='Shows only monorail issues that have completed (Fixed|Verified) '
856 'by the user.')
857 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000858 '-o', '--output', metavar='<file>',
859 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000860
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000861 # Remove description formatting
862 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800863 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000864
865 options, args = parser.parse_args()
866 options.local_user = os.environ.get('USER')
867 if args:
868 parser.error('Args unsupported')
869 if not options.user:
Bruce Dawson84370962019-02-21 05:33:37 +0000870 parser.error('USER/USERNAME is not set, please use -u')
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000871 # Retains the original -u option as the email address.
872 options.email = options.user
873 options.user = username(options.email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000874
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000875 logging.basicConfig(level=options.verbosity)
876
877 # python-keyring provides easy access to the system keyring.
878 try:
879 import keyring # pylint: disable=unused-import,unused-variable,F0401
880 except ImportError:
881 logging.warning('Consider installing python-keyring')
882
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000883 if not options.begin:
884 if options.last_quarter:
885 begin, end = quarter_begin, quarter_end
886 elif options.this_year:
887 begin, end = get_year_of(datetime.today())
888 elif options.week_of:
889 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000890 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000891 begin, end = (get_week_of(datetime.today() -
892 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000893 else:
894 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
895 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700896 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000897 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700898 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000899 else:
900 end = datetime.today()
901 options.begin, options.end = begin, end
Bruce Dawson2afcf222019-02-25 22:07:08 +0000902 if begin >= end:
903 # The queries fail in peculiar ways when the begin date is in the future.
904 # Give a descriptive error message instead.
905 logging.error('Start date (%s) is the same or later than end date (%s)' %
906 (begin, end))
907 return 1
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000908
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000909 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000910 options.output_format_heading = '### {heading}\n'
911 options.output_format = ' * [{title}]({url})'
912 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000913 logging.info('Searching for activity by %s', options.user)
914 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000915
Nicolas Boichatcd3696c2021-06-02 01:42:18 +0000916 if options.config_file:
917 with open(options.config_file) as f:
918 config = json.load(f)
919
920 for item, entries in config.items():
921 if item == 'gerrit_instances':
922 for repo, dic in entries.items():
923 # Use property name as URL
924 dic['url'] = repo
925 gerrit_instances.append(dic)
926 elif item == 'monorail_projects':
927 monorail_projects.append(entries)
928 else:
929 logging.error('Invalid entry in config file.')
930 return 1
931
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000932 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100933 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000934
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100935 if not (options.changes or options.reviews or options.issues or
936 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000937 options.changes = True
938 options.issues = True
939 options.reviews = True
940
941 # First do any required authentication so none of the user interaction has to
942 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100943 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000944 my_activity.auth_for_changes()
945 if options.reviews:
946 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000947
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000948 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000949
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000950 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100951 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000952 my_activity.get_changes()
953 if options.reviews:
954 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100955 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000956 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100957 if not options.no_referenced_issues:
958 my_activity.get_referenced_issues()
Edward Lemur5b929a42019-10-21 17:57:39 +0000959 except auth.LoginRequiredError as e:
960 logging.error('auth.LoginRequiredError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000961
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100962 my_activity.show_progress('\n')
963
Vadim Bendebury8de38002018-05-14 19:02:55 -0700964 my_activity.print_access_errors()
965
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000966 output_file = None
967 try:
968 if options.output:
969 output_file = open(options.output, 'w')
970 logging.info('Printing output to "%s"', options.output)
971 sys.stdout = output_file
972 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700973 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000974 else:
975 if options.json:
976 my_activity.dump_json()
977 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100978 if options.changes:
979 my_activity.print_changes()
980 if options.reviews:
981 my_activity.print_reviews()
982 if options.issues:
983 my_activity.print_issues()
984 if options.changes_by_issue:
985 my_activity.print_changes_by_issue(
986 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000987 finally:
988 if output_file:
989 logging.info('Done printing to file.')
990 sys.stdout = sys.__stdout__
991 output_file.close()
992
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000993 return 0
994
995
996if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000997 # Fix encoding to support non-ascii issue titles.
998 fix_encoding.fix_encoding()
999
sbc@chromium.org013731e2015-02-26 18:28:43 +00001000 try:
1001 sys.exit(main())
1002 except KeyboardInterrupt:
1003 sys.stderr.write('interrupted\n')
1004 sys.exit(1)