blob: 3fdd7cf8d106c36a286ffc6446a8c468e0e8879d [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
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +000060
Edward Lemur2a048032020-01-14 22:58:13 +000061if sys.version_info.major == 2:
Edward Lemura3b6fd02020-03-02 22:16:15 +000062 logging.warning(
63 'Python 2 is deprecated. Run my_activity.py using vpython3.')
Edward Lemur2a048032020-01-14 22:58:13 +000064 import urllib as urllib_parse
65else:
66 import urllib.parse as urllib_parse
67
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000068try:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000069 import dateutil # pylint: disable=import-error
70 import dateutil.parser
71 from dateutil.relativedelta import relativedelta
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000072except ImportError:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000073 logging.error('python-dateutil package required')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000074 exit(1)
75
Tobias Sargeantffb3c432017-03-08 14:09:14 +000076
77class DefaultFormatter(Formatter):
78 def __init__(self, default = ''):
79 super(DefaultFormatter, self).__init__()
80 self.default = default
81
82 def get_value(self, key, args, kwds):
Edward Lemur2a048032020-01-14 22:58:13 +000083 if isinstance(key, str) and key not in kwds:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000084 return self.default
85 return Formatter.get_value(self, key, args, kwds)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000086
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000087gerrit_instances = [
88 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000089 'url': 'android-review.googlesource.com',
Mike Frysingerfdf027a2020-02-27 21:53:45 +000090 'shorturl': 'r.android.com',
91 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000092 },
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000093 {
Mike Frysinger24fd8632020-02-27 22:07:06 +000094 'url': 'gerrit-review.googlesource.com',
95 },
96 {
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000097 'url': 'chrome-internal-review.googlesource.com',
Jeremy Romanf475a472017-06-06 16:49:11 -040098 'shorturl': 'crrev.com/i',
Varun Khanejad9f97bc2017-08-02 12:55:01 -070099 'short_url_protocol': 'https',
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000100 },
deymo@chromium.org56dc57a2015-09-10 18:26:54 +0000101 {
Adrienne Walker95d4c852018-09-27 20:28:12 +0000102 'url': 'chromium-review.googlesource.com',
103 'shorturl': 'crrev.com/c',
104 'short_url_protocol': 'https',
deymo@chromium.org56dc57a2015-09-10 18:26:54 +0000105 },
Ryan Harrison897602a2017-09-18 16:23:41 -0400106 {
Ryan Harrison06e18692019-09-23 18:22:25 +0000107 'url': 'dawn-review.googlesource.com',
108 },
109 {
Ryan Harrison897602a2017-09-18 16:23:41 -0400110 'url': 'pdfium-review.googlesource.com',
111 },
Adrienne Walker95d4c852018-09-27 20:28:12 +0000112 {
113 'url': 'skia-review.googlesource.com',
114 },
Paul Fagerburgb93d82c2020-08-17 16:19:46 +0000115 {
116 'url': 'review.coreboot.org',
117 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000118]
119
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100120monorail_projects = {
Jamie Madill1db68ea2019-09-03 19:34:42 +0000121 'angleproject': {
122 'shorturl': 'anglebug.com',
123 'short_url_protocol': 'http',
124 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100125 'chromium': {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000126 'shorturl': 'crbug.com',
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700127 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000128 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000129 'dawn': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100130 'google-breakpad': {},
131 'gyp': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100132 'pdfium': {
Ryan Harrison897602a2017-09-18 16:23:41 -0400133 'shorturl': 'crbug.com/pdfium',
134 'short_url_protocol': 'https',
135 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000136 'skia': {},
Ryan Harrison97811152021-03-29 20:30:57 +0000137 'tint': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100138 'v8': {
139 'shorturl': 'crbug.com/v8',
140 'short_url_protocol': 'https',
141 },
142}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000143
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000144def username(email):
145 """Keeps the username of an email address."""
146 return email and email.split('@', 1)[0]
147
148
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000149def datetime_to_midnight(date):
150 return date - timedelta(hours=date.hour, minutes=date.minute,
151 seconds=date.second, microseconds=date.microsecond)
152
153
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000154def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000155 begin = (datetime_to_midnight(date) -
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000156 relativedelta(months=(date.month - 1) % 3, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000157 return begin, begin + relativedelta(months=3)
158
159
160def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000161 begin = (datetime_to_midnight(date) -
162 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000163 return begin, begin + relativedelta(years=1)
164
165
166def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000167 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000168 return begin, begin + timedelta(days=7)
169
170
171def get_yes_or_no(msg):
172 while True:
Edward Lesmesae3586b2020-03-23 21:21:14 +0000173 response = gclient_utils.AskForData(msg + ' yes/no [no] ')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000174 if response == 'y' or response == 'yes':
175 return True
176 elif not response or response == 'n' or response == 'no':
177 return False
178
179
deymo@chromium.org6c039202013-09-12 12:28:12 +0000180def datetime_from_gerrit(date_string):
181 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
182
183
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100184def datetime_from_monorail(date_string):
185 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000186
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000187def extract_bug_numbers_from_description(issue):
188 # Getting the description for REST Gerrit
189 revision = issue['revisions'][issue['current_revision']]
190 description = revision['commit']['message']
191
192 bugs = []
193 # Handle both "Bug: 99999" and "BUG=99999" bug notations
194 # Multiple bugs can be noted on a single line or in multiple ones.
195 matches = re.findall(
196 r'^(BUG=|(Bug|Fixed):\s*)((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)',
197 description, flags=re.IGNORECASE | re.MULTILINE)
198 if matches:
199 for match in matches:
200 bugs.extend(match[2].replace(' ', '').split(','))
201 # Add default chromium: prefix if none specified.
202 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
203
204 return sorted(set(bugs))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000205
206class MyActivity(object):
207 def __init__(self, options):
208 self.options = options
209 self.modified_after = options.begin
210 self.modified_before = options.end
211 self.user = options.user
212 self.changes = []
213 self.reviews = []
214 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100215 self.referenced_issues = []
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000216 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-14 19:02:55 -0700217 self.access_errors = set()
Vadim Bendebury80012972019-11-23 02:23:11 +0000218 self.skip_servers = (options.skip_servers.split(','))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000219
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100220 def show_progress(self, how='.'):
221 if sys.stdout.isatty():
222 sys.stdout.write(how)
223 sys.stdout.flush()
224
Vadim Bendebury8de38002018-05-14 19:02:55 -0700225 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200226 # Convert the "key:value" filter to a list of (key, value) pairs.
227 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000228 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000229 # Instantiate the generator to force all the requests now and catch the
230 # errors here.
231 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000232 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
233 'CURRENT_REVISION', 'CURRENT_COMMIT']))
Raul Tambre7c938462019-05-24 16:35:35 +0000234 except gerrit_util.GerritError as e:
Vadim Bendebury8de38002018-05-14 19:02:55 -0700235 error_message = 'Looking up %r: %s' % (instance['url'], e)
236 if error_message not in self.access_errors:
237 self.access_errors.add(error_message)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000238 return []
239
deymo@chromium.org6c039202013-09-12 12:28:12 +0000240 def gerrit_search(self, instance, owner=None, reviewer=None):
Vadim Bendebury80012972019-11-23 02:23:11 +0000241 if instance['url'] in self.skip_servers:
242 return []
deymo@chromium.org6c039202013-09-12 12:28:12 +0000243 max_age = datetime.today() - self.modified_after
Andrii Shyshkalov4aedec02018-09-17 16:31:17 +0000244 filters = ['-age:%ss' % (max_age.days * 24 * 3600 + max_age.seconds)]
245 if owner:
246 assert not reviewer
247 filters.append('owner:%s' % owner)
248 else:
249 filters.extend(('-owner:%s' % reviewer, 'reviewer:%s' % reviewer))
Peter K. Lee9342ac02018-08-28 21:03:51 +0000250 # TODO(cjhopman): Should abandoned changes be filtered out when
251 # merged_only is not enabled?
252 if self.options.merged_only:
253 filters.append('status:merged')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000254
Aaron Gable2979a872017-09-05 17:38:32 -0700255 issues = self.gerrit_changes_over_rest(instance, filters)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100256 self.show_progress()
Aaron Gable2979a872017-09-05 17:38:32 -0700257 issues = [self.process_gerrit_issue(instance, issue)
258 for issue in issues]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000259
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000260 issues = filter(self.filter_issue, issues)
261 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
262
263 return issues
264
Aaron Gable2979a872017-09-05 17:38:32 -0700265 def process_gerrit_issue(self, instance, issue):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000266 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000267 if self.options.deltas:
268 ret['delta'] = DefaultFormatter().format(
269 '+{insertions},-{deletions}',
270 **issue)
271 ret['status'] = issue['status']
deymo@chromium.org6c039202013-09-12 12:28:12 +0000272 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700273 protocol = instance.get('short_url_protocol', 'http')
274 url = instance['shorturl']
275 else:
276 protocol = 'https'
277 url = instance['url']
278 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
279
deymo@chromium.org6c039202013-09-12 12:28:12 +0000280 ret['header'] = issue['subject']
Don Garrett2ebf9fd2018-08-06 15:14:00 +0000281 ret['owner'] = issue['owner'].get('email', '')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000282 ret['author'] = ret['owner']
283 ret['created'] = datetime_from_gerrit(issue['created'])
284 ret['modified'] = datetime_from_gerrit(issue['updated'])
285 if 'messages' in issue:
Aaron Gable2979a872017-09-05 17:38:32 -0700286 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
deymo@chromium.org6c039202013-09-12 12:28:12 +0000287 else:
288 ret['replies'] = []
289 ret['reviewers'] = set(r['author'] for r in ret['replies'])
290 ret['reviewers'].discard(ret['author'])
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000291 ret['bugs'] = extract_bug_numbers_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000292 return ret
293
294 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700295 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000296 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000297 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
298 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000299 for reply in replies:
300 ret.append({
301 'author': reply['author']['email'],
302 'created': datetime_from_gerrit(reply['date']),
303 'content': reply['message'],
304 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000305 return ret
306
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100307 def monorail_get_auth_http(self):
Kenneth Russell66badbd2018-09-09 21:35:32 +0000308 # Manually use a long timeout (10m); for some users who have a
309 # long history on the issue tracker, whatever the default timeout
310 # is is reached.
Edward Lemur5b929a42019-10-21 17:57:39 +0000311 return auth.Authenticator().authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100312
313 def filter_modified_monorail_issue(self, issue):
314 """Precisely checks if an issue has been modified in the time range.
315
316 This fetches all issue comments to check if the issue has been modified in
317 the time range specified by user. This is needed because monorail only
318 allows filtering by last updated and published dates, which is not
319 sufficient to tell whether a given issue has been modified at some specific
320 time range. Any update to the issue is a reported as comment on Monorail.
321
322 Args:
323 issue: Issue dict as returned by monorail_query_issues method. In
324 particular, must have a key 'uid' formatted as 'project:issue_id'.
325
326 Returns:
327 Passed issue if modified, None otherwise.
328 """
329 http = self.monorail_get_auth_http()
330 project, issue_id = issue['uid'].split(':')
331 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
332 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
333 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100334 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100335 content = json.loads(body)
336 if not content:
337 logging.error('Unable to parse %s response from monorail.', project)
338 return issue
339
340 for item in content.get('items', []):
341 comment_published = datetime_from_monorail(item['published'])
342 if self.filter_modified(comment_published):
343 return issue
344
345 return None
346
347 def monorail_query_issues(self, project, query):
348 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000349 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100350 '/%s/issues') % project
Edward Lemur2a048032020-01-14 22:58:13 +0000351 query_data = urllib_parse.urlencode(query)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100352 url = url + '?' + query_data
353 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100354 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100355 content = json.loads(body)
356 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100357 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100358 return []
359
360 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100361 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100362 for item in content.get('items', []):
363 if project_config.get('shorturl'):
364 protocol = project_config.get('short_url_protocol', 'http')
365 item_url = '%s://%s/%d' % (
366 protocol, project_config['shorturl'], item['id'])
367 else:
368 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
369 project, item['id'])
370 issue = {
371 'uid': '%s:%s' % (project, item['id']),
372 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100373 'created': datetime_from_monorail(item['published']),
374 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100375 'author': item['author']['name'],
376 'url': item_url,
377 'comments': [],
378 'status': item['status'],
379 'labels': [],
380 'components': []
381 }
382 if 'owner' in item:
383 issue['owner'] = item['owner']['name']
384 else:
385 issue['owner'] = 'None'
386 if 'labels' in item:
387 issue['labels'] = item['labels']
388 if 'components' in item:
389 issue['components'] = item['components']
390 issues.append(issue)
391
392 return issues
393
394 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000395 epoch = datetime.utcfromtimestamp(0)
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000396 # Defaults to @chromium.org email if one wasn't provided on -u option.
397 user_str = (self.options.email if self.options.email.find('@') >= 0
398 else '%s@chromium.org' % self.user)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000399
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100400 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000401 'maxResults': 10000,
402 'q': user_str,
403 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
404 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000405 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000406
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000407 if self.options.completed_issues:
408 return [
409 issue for issue in issues
410 if (self.match(issue['owner']) and
411 issue['status'].lower() in ('verified', 'fixed'))
412 ]
413
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100414 return [
415 issue for issue in issues
416 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000417
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100418 def monorail_get_issues(self, project, issue_ids):
419 return self.monorail_query_issues(project, {
420 'maxResults': 10000,
421 'q': 'id:%s' % ','.join(issue_ids)
422 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000423
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000424 def print_heading(self, heading):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000425 print()
426 print(self.options.output_format_heading.format(heading=heading))
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000427
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000428 def match(self, author):
429 if '@' in self.user:
430 return author == self.user
431 return author.startswith(self.user + '@')
432
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000433 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000434 activity = len([
435 reply
436 for reply in change['replies']
437 if self.match(reply['author'])
438 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000439 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000440 'created': change['created'].date().isoformat(),
441 'modified': change['modified'].date().isoformat(),
442 'reviewers': ', '.join(change['reviewers']),
443 'status': change['status'],
444 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000445 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000446 if self.options.deltas:
447 optional_values['delta'] = change['delta']
448
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000449 self.print_generic(self.options.output_format,
450 self.options.output_format_changes,
451 change['header'],
452 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000453 change['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000454 change['created'],
455 change['modified'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000456 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000457
458 def print_issue(self, issue):
459 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000460 'created': issue['created'].date().isoformat(),
461 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000462 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000463 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000464 }
465 self.print_generic(self.options.output_format,
466 self.options.output_format_issues,
467 issue['header'],
468 issue['url'],
469 issue['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000470 issue['created'],
471 issue['modified'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000472 optional_values)
473
474 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000475 activity = len([
476 reply
477 for reply in review['replies']
478 if self.match(reply['author'])
479 ])
480 optional_values = {
481 'created': review['created'].date().isoformat(),
482 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800483 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000484 'activity': activity,
485 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800486 if self.options.deltas:
487 optional_values['delta'] = review['delta']
488
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000489 self.print_generic(self.options.output_format,
490 self.options.output_format_reviews,
491 review['header'],
492 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000493 review['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000494 review['created'],
495 review['modified'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000496 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000497
498 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000499 def print_generic(default_fmt, specific_fmt,
Lutz Justen860378e2019-03-12 01:10:02 +0000500 title, url, author, created, modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000501 optional_values=None):
502 output_format = specific_fmt if specific_fmt is not None else default_fmt
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000503 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000504 'title': title,
505 'url': url,
506 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000507 'created': created,
508 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000509 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000510 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000511 values.update(optional_values)
Edward Lemur2a048032020-01-14 22:58:13 +0000512 print(DefaultFormatter().format(output_format, **values))
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000513
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000514
515 def filter_issue(self, issue, should_filter_by_user=True):
516 def maybe_filter_username(email):
517 return not should_filter_by_user or username(email) == self.user
518 if (maybe_filter_username(issue['author']) and
519 self.filter_modified(issue['created'])):
520 return True
521 if (maybe_filter_username(issue['owner']) and
522 (self.filter_modified(issue['created']) or
523 self.filter_modified(issue['modified']))):
524 return True
525 for reply in issue['replies']:
526 if self.filter_modified(reply['created']):
527 if not should_filter_by_user:
528 break
529 if (username(reply['author']) == self.user
530 or (self.user + '@') in reply['content']):
531 break
532 else:
533 return False
534 return True
535
536 def filter_modified(self, modified):
537 return self.modified_after < modified and modified < self.modified_before
538
539 def auth_for_changes(self):
540 #TODO(cjhopman): Move authentication check for getting changes here.
541 pass
542
543 def auth_for_reviews(self):
544 # Reviews use all the same instances as changes so no authentication is
545 # required.
546 pass
547
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000548 def get_changes(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000549 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100550 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100551 gerrit_changes = pool.map_async(
552 lambda instance: self.gerrit_search(instance, owner=self.user),
553 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100554 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000555 self.changes = list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000556
557 def print_changes(self):
558 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000559 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000560 for change in self.changes:
Bruce Dawson2afcf222019-02-25 22:07:08 +0000561 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000562
Vadim Bendebury8de38002018-05-14 19:02:55 -0700563 def print_access_errors(self):
564 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400565 logging.error('Access Errors:')
566 for error in self.access_errors:
567 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700568
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000569 def get_reviews(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000570 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100571 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100572 gerrit_reviews = pool.map_async(
573 lambda instance: self.gerrit_search(instance, reviewer=self.user),
574 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100575 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000576 self.reviews = list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000577
578 def print_reviews(self):
579 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000580 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000581 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000582 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000583
584 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100585 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
586 monorail_issues = pool.map(
587 self.monorail_issue_search, monorail_projects.keys())
588 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
589
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700590 if not monorail_issues:
591 return
592
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100593 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
594 filtered_issues = pool.map(
595 self.filter_modified_monorail_issue, monorail_issues)
596 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100597
598 def get_referenced_issues(self):
599 if not self.issues:
600 self.get_issues()
601
602 if not self.changes:
603 self.get_changes()
604
605 referenced_issue_uids = set(itertools.chain.from_iterable(
606 change['bugs'] for change in self.changes))
607 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
608 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
609
610 missing_issues_by_project = collections.defaultdict(list)
611 for issue_uid in missing_issue_uids:
612 project, issue_id = issue_uid.split(':')
613 missing_issues_by_project[project].append(issue_id)
614
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000615 for project, issue_ids in missing_issues_by_project.items():
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100616 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000617
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000618 def print_issues(self):
619 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000620 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000621 for issue in self.issues:
622 self.print_issue(issue)
623
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100624 def print_changes_by_issue(self, skip_empty_own):
625 if not self.issues or not self.changes:
626 return
627
628 self.print_heading('Changes by referenced issue(s)')
629 issues = {issue['uid']: issue for issue in self.issues}
630 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
631 changes_by_issue_uid = collections.defaultdict(list)
632 changes_by_ref_issue_uid = collections.defaultdict(list)
633 changes_without_issue = []
634 for change in self.changes:
635 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000636 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100637 if issue_uid in issues:
638 changes_by_issue_uid[issue_uid].append(change)
639 added = True
640 if issue_uid in ref_issues:
641 changes_by_ref_issue_uid[issue_uid].append(change)
642 added = True
643 if not added:
644 changes_without_issue.append(change)
645
646 # Changes referencing own issues.
647 for issue_uid in issues:
648 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
649 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000650 if changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000651 print()
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000652 for change in changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000653 print(' ', end='') # this prints no newline
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000654 self.print_change(change)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000655 print()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100656
657 # Changes referencing others' issues.
658 for issue_uid in ref_issues:
659 assert changes_by_ref_issue_uid[issue_uid]
660 self.print_issue(ref_issues[issue_uid])
661 for change in changes_by_ref_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000662 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100663 self.print_change(change)
664
665 # Changes referencing no issues.
666 if changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000667 print(self.options.output_format_no_url.format(title='Other changes'))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100668 for change in changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000669 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100670 self.print_change(change)
671
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000672 def print_activity(self):
673 self.print_changes()
674 self.print_reviews()
675 self.print_issues()
676
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000677 def dump_json(self, ignore_keys=None):
678 if ignore_keys is None:
679 ignore_keys = ['replies']
680
681 def format_for_json_dump(in_array):
682 output = {}
683 for item in in_array:
684 url = item.get('url') or item.get('review_url')
685 if not url:
686 raise Exception('Dumped item %s does not specify url' % item)
687 output[url] = dict(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000688 (k, v) for k,v in item.items() if k not in ignore_keys)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000689 return output
690
691 class PythonObjectEncoder(json.JSONEncoder):
692 def default(self, obj): # pylint: disable=method-hidden
693 if isinstance(obj, datetime):
694 return obj.isoformat()
695 if isinstance(obj, set):
696 return list(obj)
697 return json.JSONEncoder.default(self, obj)
698
699 output = {
700 'reviews': format_for_json_dump(self.reviews),
701 'changes': format_for_json_dump(self.changes),
702 'issues': format_for_json_dump(self.issues)
703 }
Raul Tambre80ee78e2019-05-06 22:41:05 +0000704 print(json.dumps(output, indent=2, cls=PythonObjectEncoder))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000705
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000706
707def main():
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000708 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
709 parser.add_option(
710 '-u', '--user', metavar='<email>',
Bruce Dawson84370962019-02-21 05:33:37 +0000711 # Look for USER and USERNAME (Windows) environment variables.
712 default=os.environ.get('USER', os.environ.get('USERNAME')),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000713 help='Filter on user, default=%default')
714 parser.add_option(
715 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000716 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000717 parser.add_option(
718 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000719 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000720 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
721 relativedelta(months=2))
722 parser.add_option(
723 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000724 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000725 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
726 parser.add_option(
727 '-Y', '--this_year', action='store_true',
728 help='Use this year\'s dates')
729 parser.add_option(
730 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000731 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000732 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000733 '-W', '--last_week', action='count',
734 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000735 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000736 '-a', '--auth',
737 action='store_true',
738 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000739 parser.add_option(
740 '-d', '--deltas',
741 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800742 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100743 parser.add_option(
744 '--no-referenced-issues',
745 action='store_true',
746 help='Do not fetch issues referenced by owned changes. Useful in '
747 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100748 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100749 parser.add_option(
Vadim Bendebury80012972019-11-23 02:23:11 +0000750 '--skip_servers',
751 action='store',
752 default='',
753 help='A comma separated list of gerrit and rietveld servers to ignore')
754 parser.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100755 '--skip-own-issues-without-changes',
756 action='store_true',
757 help='Skips listing own issues without changes when showing changes '
758 'grouped by referenced issue(s). See --changes-by-issue for more '
759 'details.')
Nicolas Boichatcd3696c2021-06-02 01:42:18 +0000760 parser.add_option(
761 '-F', '--config_file', metavar='<config_file>',
762 help='Configuration file in JSON format, used to add additional gerrit '
763 'instances (see source code for an example).')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000764
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000765 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000766 'By default, all activity will be looked up and '
767 'printed. If any of these are specified, only '
768 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000769 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000770 '-c', '--changes',
771 action='store_true',
772 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000773 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000774 '-i', '--issues',
775 action='store_true',
776 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000777 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000778 '-r', '--reviews',
779 action='store_true',
780 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100781 activity_types_group.add_option(
782 '--changes-by-issue', action='store_true',
783 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000784 parser.add_option_group(activity_types_group)
785
786 output_format_group = optparse.OptionGroup(parser, 'Output Format',
787 'By default, all activity will be printed in the '
788 'following format: {url} {title}. This can be '
789 'changed for either all activity types or '
790 'individually for each activity type. The format '
791 'is defined as documented for '
792 'string.format(...). The variables available for '
Lutz Justen860378e2019-03-12 01:10:02 +0000793 'all activity types are url, title, author, '
794 'created and modified. Format options for '
795 'specific activity types will override the '
796 'generic format.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000797 output_format_group.add_option(
798 '-f', '--output-format', metavar='<format>',
799 default=u'{url} {title}',
800 help='Specifies the format to use when printing all your activity.')
801 output_format_group.add_option(
802 '--output-format-changes', metavar='<format>',
803 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000804 help='Specifies the format to use when printing changes. Supports the '
805 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000806 output_format_group.add_option(
807 '--output-format-issues', metavar='<format>',
808 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000809 help='Specifies the format to use when printing issues. Supports the '
810 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000811 output_format_group.add_option(
812 '--output-format-reviews', metavar='<format>',
813 default=None,
814 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000815 output_format_group.add_option(
816 '--output-format-heading', metavar='<format>',
817 default=u'{heading}:',
Vincent Scheib2be61a12020-05-26 16:45:32 +0000818 help='Specifies the format to use when printing headings. '
819 'Supports the variable {heading}.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000820 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100821 '--output-format-no-url', default='{title}',
822 help='Specifies the format to use when printing activity without url.')
823 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000824 '-m', '--markdown', action='store_true',
825 help='Use markdown-friendly output (overrides --output-format '
826 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000827 output_format_group.add_option(
828 '-j', '--json', action='store_true',
829 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000830 parser.add_option_group(output_format_group)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000831
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000832 parser.add_option(
833 '-v', '--verbose',
834 action='store_const',
835 dest='verbosity',
836 default=logging.WARN,
837 const=logging.INFO,
838 help='Output extra informational messages.'
839 )
840 parser.add_option(
841 '-q', '--quiet',
842 action='store_const',
843 dest='verbosity',
844 const=logging.ERROR,
845 help='Suppress non-error messages.'
846 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000847 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000848 '-M', '--merged-only',
849 action='store_true',
850 dest='merged_only',
851 default=False,
852 help='Shows only changes that have been merged.')
853 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000854 '-C', '--completed-issues',
855 action='store_true',
856 dest='completed_issues',
857 default=False,
858 help='Shows only monorail issues that have completed (Fixed|Verified) '
859 'by the user.')
860 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000861 '-o', '--output', metavar='<file>',
862 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000863
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000864 # Remove description formatting
865 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800866 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000867
868 options, args = parser.parse_args()
869 options.local_user = os.environ.get('USER')
870 if args:
871 parser.error('Args unsupported')
872 if not options.user:
Bruce Dawson84370962019-02-21 05:33:37 +0000873 parser.error('USER/USERNAME is not set, please use -u')
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000874 # Retains the original -u option as the email address.
875 options.email = options.user
876 options.user = username(options.email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000877
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000878 logging.basicConfig(level=options.verbosity)
879
880 # python-keyring provides easy access to the system keyring.
881 try:
882 import keyring # pylint: disable=unused-import,unused-variable,F0401
883 except ImportError:
884 logging.warning('Consider installing python-keyring')
885
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000886 if not options.begin:
887 if options.last_quarter:
888 begin, end = quarter_begin, quarter_end
889 elif options.this_year:
890 begin, end = get_year_of(datetime.today())
891 elif options.week_of:
892 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000893 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000894 begin, end = (get_week_of(datetime.today() -
895 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000896 else:
897 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
898 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700899 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000900 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700901 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000902 else:
903 end = datetime.today()
904 options.begin, options.end = begin, end
Bruce Dawson2afcf222019-02-25 22:07:08 +0000905 if begin >= end:
906 # The queries fail in peculiar ways when the begin date is in the future.
907 # Give a descriptive error message instead.
908 logging.error('Start date (%s) is the same or later than end date (%s)' %
909 (begin, end))
910 return 1
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000911
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000912 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000913 options.output_format_heading = '### {heading}\n'
914 options.output_format = ' * [{title}]({url})'
915 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000916 logging.info('Searching for activity by %s', options.user)
917 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000918
Nicolas Boichatcd3696c2021-06-02 01:42:18 +0000919 if options.config_file:
920 with open(options.config_file) as f:
921 config = json.load(f)
922
923 for item, entries in config.items():
924 if item == 'gerrit_instances':
925 for repo, dic in entries.items():
926 # Use property name as URL
927 dic['url'] = repo
928 gerrit_instances.append(dic)
929 elif item == 'monorail_projects':
930 monorail_projects.append(entries)
931 else:
932 logging.error('Invalid entry in config file.')
933 return 1
934
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000935 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100936 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000937
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100938 if not (options.changes or options.reviews or options.issues or
939 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000940 options.changes = True
941 options.issues = True
942 options.reviews = True
943
944 # First do any required authentication so none of the user interaction has to
945 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100946 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000947 my_activity.auth_for_changes()
948 if options.reviews:
949 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000950
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000951 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000952
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000953 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100954 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000955 my_activity.get_changes()
956 if options.reviews:
957 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100958 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000959 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100960 if not options.no_referenced_issues:
961 my_activity.get_referenced_issues()
Edward Lemur5b929a42019-10-21 17:57:39 +0000962 except auth.LoginRequiredError as e:
963 logging.error('auth.LoginRequiredError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000964
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100965 my_activity.show_progress('\n')
966
Vadim Bendebury8de38002018-05-14 19:02:55 -0700967 my_activity.print_access_errors()
968
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000969 output_file = None
970 try:
971 if options.output:
972 output_file = open(options.output, 'w')
973 logging.info('Printing output to "%s"', options.output)
974 sys.stdout = output_file
975 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700976 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000977 else:
978 if options.json:
979 my_activity.dump_json()
980 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100981 if options.changes:
982 my_activity.print_changes()
983 if options.reviews:
984 my_activity.print_reviews()
985 if options.issues:
986 my_activity.print_issues()
987 if options.changes_by_issue:
988 my_activity.print_changes_by_issue(
989 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000990 finally:
991 if output_file:
992 logging.info('Done printing to file.')
993 sys.stdout = sys.__stdout__
994 output_file.close()
995
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000996 return 0
997
998
999if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +00001000 # Fix encoding to support non-ascii issue titles.
1001 fix_encoding.fix_encoding()
1002
sbc@chromium.org013731e2015-02-26 18:28:43 +00001003 try:
1004 sys.exit(main())
1005 except KeyboardInterrupt:
1006 sys.stderr.write('interrupted\n')
1007 sys.exit(1)