blob: 7fb0f1cf62bc3297c08f823b7ecb282a030d833a [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')
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +000071 sys.exit(1)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000072
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
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +000079 def get_value(self, key, args, kwargs):
80 if isinstance(key, str) and key not in kwargs:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000081 return self.default
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +000082 return Formatter.get_value(self, key, args, kwargs)
83
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000084
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000085gerrit_instances = [
86 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000087 'url': 'android-review.googlesource.com',
Mike Frysingerfdf027a2020-02-27 21:53:45 +000088 'shorturl': 'r.android.com',
89 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000090 },
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000091 {
Mike Frysinger24fd8632020-02-27 22:07:06 +000092 'url': 'gerrit-review.googlesource.com',
93 },
94 {
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000095 'url': 'chrome-internal-review.googlesource.com',
Jeremy Romanf475a472017-06-06 16:49:11 -040096 'shorturl': 'crrev.com/i',
Varun Khanejad9f97bc2017-08-02 12:55:01 -070097 'short_url_protocol': 'https',
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000098 },
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000099 {
Adrienne Walker95d4c852018-09-27 20:28:12 +0000100 'url': 'chromium-review.googlesource.com',
101 'shorturl': 'crrev.com/c',
102 'short_url_protocol': 'https',
deymo@chromium.org56dc57a2015-09-10 18:26:54 +0000103 },
Ryan Harrison897602a2017-09-18 16:23:41 -0400104 {
Ryan Harrison06e18692019-09-23 18:22:25 +0000105 'url': 'dawn-review.googlesource.com',
106 },
107 {
Ryan Harrison897602a2017-09-18 16:23:41 -0400108 'url': 'pdfium-review.googlesource.com',
109 },
Adrienne Walker95d4c852018-09-27 20:28:12 +0000110 {
111 'url': 'skia-review.googlesource.com',
112 },
Paul Fagerburgb93d82c2020-08-17 16:19:46 +0000113 {
114 'url': 'review.coreboot.org',
115 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000116]
117
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100118monorail_projects = {
Jamie Madill1db68ea2019-09-03 19:34:42 +0000119 'angleproject': {
120 'shorturl': 'anglebug.com',
121 'short_url_protocol': 'http',
122 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100123 'chromium': {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000124 'shorturl': 'crbug.com',
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700125 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000126 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000127 'dawn': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100128 'google-breakpad': {},
129 'gyp': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100130 'pdfium': {
Ryan Harrison897602a2017-09-18 16:23:41 -0400131 'shorturl': 'crbug.com/pdfium',
132 'short_url_protocol': 'https',
133 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000134 'skia': {},
Ryan Harrison97811152021-03-29 20:30:57 +0000135 'tint': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100136 'v8': {
137 'shorturl': 'crbug.com/v8',
138 'short_url_protocol': 'https',
139 },
140}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000141
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000142def username(email):
143 """Keeps the username of an email address."""
144 return email and email.split('@', 1)[0]
145
146
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000147def datetime_to_midnight(date):
148 return date - timedelta(hours=date.hour, minutes=date.minute,
149 seconds=date.second, microseconds=date.microsecond)
150
151
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000152def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000153 begin = (datetime_to_midnight(date) -
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000154 relativedelta(months=(date.month - 1) % 3, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000155 return begin, begin + relativedelta(months=3)
156
157
158def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000159 begin = (datetime_to_midnight(date) -
160 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000161 return begin, begin + relativedelta(years=1)
162
163
164def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000165 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000166 return begin, begin + timedelta(days=7)
167
168
169def get_yes_or_no(msg):
170 while True:
Edward Lesmesae3586b2020-03-23 21:21:14 +0000171 response = gclient_utils.AskForData(msg + ' yes/no [no] ')
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000172 if response in ('y', 'yes'):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000173 return True
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000174
175 if not response or response in ('n', 'no'):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000176 return False
177
178
deymo@chromium.org6c039202013-09-12 12:28:12 +0000179def datetime_from_gerrit(date_string):
180 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
181
182
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100183def datetime_from_monorail(date_string):
184 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000185
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000186def extract_bug_numbers_from_description(issue):
187 # Getting the description for REST Gerrit
188 revision = issue['revisions'][issue['current_revision']]
189 description = revision['commit']['message']
190
191 bugs = []
192 # Handle both "Bug: 99999" and "BUG=99999" bug notations
193 # Multiple bugs can be noted on a single line or in multiple ones.
194 matches = re.findall(
195 r'^(BUG=|(Bug|Fixed):\s*)((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)',
196 description, flags=re.IGNORECASE | re.MULTILINE)
197 if matches:
198 for match in matches:
199 bugs.extend(match[2].replace(' ', '').split(','))
200 # Add default chromium: prefix if none specified.
201 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
202
203 return sorted(set(bugs))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000204
205class MyActivity(object):
206 def __init__(self, options):
207 self.options = options
208 self.modified_after = options.begin
209 self.modified_before = options.end
210 self.user = options.user
211 self.changes = []
212 self.reviews = []
213 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100214 self.referenced_issues = []
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000215 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-14 19:02:55 -0700216 self.access_errors = set()
Vadim Bendebury80012972019-11-23 02:23:11 +0000217 self.skip_servers = (options.skip_servers.split(','))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000218
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100219 def show_progress(self, how='.'):
220 if sys.stdout.isatty():
221 sys.stdout.write(how)
222 sys.stdout.flush()
223
Vadim Bendebury8de38002018-05-14 19:02:55 -0700224 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200225 # Convert the "key:value" filter to a list of (key, value) pairs.
226 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000227 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000228 # Instantiate the generator to force all the requests now and catch the
229 # errors here.
230 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000231 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
232 'CURRENT_REVISION', 'CURRENT_COMMIT']))
Raul Tambre7c938462019-05-24 16:35:35 +0000233 except gerrit_util.GerritError as e:
Vadim Bendebury8de38002018-05-14 19:02:55 -0700234 error_message = 'Looking up %r: %s' % (instance['url'], e)
235 if error_message not in self.access_errors:
236 self.access_errors.add(error_message)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000237 return []
238
deymo@chromium.org6c039202013-09-12 12:28:12 +0000239 def gerrit_search(self, instance, owner=None, reviewer=None):
Vadim Bendebury80012972019-11-23 02:23:11 +0000240 if instance['url'] in self.skip_servers:
241 return []
deymo@chromium.org6c039202013-09-12 12:28:12 +0000242 max_age = datetime.today() - self.modified_after
Andrii Shyshkalov4aedec02018-09-17 16:31:17 +0000243 filters = ['-age:%ss' % (max_age.days * 24 * 3600 + max_age.seconds)]
244 if owner:
245 assert not reviewer
246 filters.append('owner:%s' % owner)
247 else:
248 filters.extend(('-owner:%s' % reviewer, 'reviewer:%s' % reviewer))
Peter K. Lee9342ac02018-08-28 21:03:51 +0000249 # TODO(cjhopman): Should abandoned changes be filtered out when
250 # merged_only is not enabled?
251 if self.options.merged_only:
252 filters.append('status:merged')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000253
Aaron Gable2979a872017-09-05 17:38:32 -0700254 issues = self.gerrit_changes_over_rest(instance, filters)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100255 self.show_progress()
Aaron Gable2979a872017-09-05 17:38:32 -0700256 issues = [self.process_gerrit_issue(instance, issue)
257 for issue in issues]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000258
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000259 issues = filter(self.filter_issue, issues)
260 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
261
262 return issues
263
Aaron Gable2979a872017-09-05 17:38:32 -0700264 def process_gerrit_issue(self, instance, issue):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000265 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000266 if self.options.deltas:
267 ret['delta'] = DefaultFormatter().format(
268 '+{insertions},-{deletions}',
269 **issue)
270 ret['status'] = issue['status']
deymo@chromium.org6c039202013-09-12 12:28:12 +0000271 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700272 protocol = instance.get('short_url_protocol', 'http')
273 url = instance['shorturl']
274 else:
275 protocol = 'https'
276 url = instance['url']
277 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
278
deymo@chromium.org6c039202013-09-12 12:28:12 +0000279 ret['header'] = issue['subject']
Don Garrett2ebf9fd2018-08-06 15:14:00 +0000280 ret['owner'] = issue['owner'].get('email', '')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000281 ret['author'] = ret['owner']
282 ret['created'] = datetime_from_gerrit(issue['created'])
283 ret['modified'] = datetime_from_gerrit(issue['updated'])
284 if 'messages' in issue:
Aaron Gable2979a872017-09-05 17:38:32 -0700285 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
deymo@chromium.org6c039202013-09-12 12:28:12 +0000286 else:
287 ret['replies'] = []
288 ret['reviewers'] = set(r['author'] for r in ret['replies'])
289 ret['reviewers'].discard(ret['author'])
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000290 ret['bugs'] = extract_bug_numbers_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000291 return ret
292
293 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700294 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000295 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000296 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
297 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000298 for reply in replies:
299 ret.append({
300 'author': reply['author']['email'],
301 'created': datetime_from_gerrit(reply['date']),
302 'content': reply['message'],
303 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000304 return ret
305
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100306 def monorail_get_auth_http(self):
Kenneth Russell66badbd2018-09-09 21:35:32 +0000307 # Manually use a long timeout (10m); for some users who have a
308 # long history on the issue tracker, whatever the default timeout
309 # is is reached.
Edward Lemur5b929a42019-10-21 17:57:39 +0000310 return auth.Authenticator().authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100311
312 def filter_modified_monorail_issue(self, issue):
313 """Precisely checks if an issue has been modified in the time range.
314
315 This fetches all issue comments to check if the issue has been modified in
316 the time range specified by user. This is needed because monorail only
317 allows filtering by last updated and published dates, which is not
318 sufficient to tell whether a given issue has been modified at some specific
319 time range. Any update to the issue is a reported as comment on Monorail.
320
321 Args:
322 issue: Issue dict as returned by monorail_query_issues method. In
323 particular, must have a key 'uid' formatted as 'project:issue_id'.
324
325 Returns:
326 Passed issue if modified, None otherwise.
327 """
328 http = self.monorail_get_auth_http()
329 project, issue_id = issue['uid'].split(':')
330 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
331 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
332 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100333 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100334 content = json.loads(body)
335 if not content:
336 logging.error('Unable to parse %s response from monorail.', project)
337 return issue
338
339 for item in content.get('items', []):
340 comment_published = datetime_from_monorail(item['published'])
341 if self.filter_modified(comment_published):
342 return issue
343
344 return None
345
346 def monorail_query_issues(self, project, query):
347 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000348 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100349 '/%s/issues') % project
Josip Sokcevic4940cc42021-10-05 23:55:34 +0000350 query_data = urllib.parse.urlencode(query)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100351 url = url + '?' + query_data
352 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100353 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100354 content = json.loads(body)
355 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100356 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100357 return []
358
359 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100360 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100361 for item in content.get('items', []):
362 if project_config.get('shorturl'):
363 protocol = project_config.get('short_url_protocol', 'http')
364 item_url = '%s://%s/%d' % (
365 protocol, project_config['shorturl'], item['id'])
366 else:
367 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
368 project, item['id'])
369 issue = {
370 'uid': '%s:%s' % (project, item['id']),
371 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100372 'created': datetime_from_monorail(item['published']),
373 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100374 'author': item['author']['name'],
375 'url': item_url,
376 'comments': [],
377 'status': item['status'],
378 'labels': [],
379 'components': []
380 }
381 if 'owner' in item:
382 issue['owner'] = item['owner']['name']
383 else:
384 issue['owner'] = 'None'
385 if 'labels' in item:
386 issue['labels'] = item['labels']
387 if 'components' in item:
388 issue['components'] = item['components']
389 issues.append(issue)
390
391 return issues
392
393 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000394 epoch = datetime.utcfromtimestamp(0)
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000395 # Defaults to @chromium.org email if one wasn't provided on -u option.
396 user_str = (self.options.email if self.options.email.find('@') >= 0
397 else '%s@chromium.org' % self.user)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000398
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100399 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000400 'maxResults': 10000,
401 'q': user_str,
402 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
403 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000404 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000405
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000406 if self.options.completed_issues:
407 return [
408 issue for issue in issues
409 if (self.match(issue['owner']) and
410 issue['status'].lower() in ('verified', 'fixed'))
411 ]
412
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100413 return [
414 issue for issue in issues
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000415 if user_str in (issue['author'], issue['owner'])]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000416
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100417 def monorail_get_issues(self, project, issue_ids):
418 return self.monorail_query_issues(project, {
419 'maxResults': 10000,
420 'q': 'id:%s' % ','.join(issue_ids)
421 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000422
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000423 def print_heading(self, heading):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000424 print()
425 print(self.options.output_format_heading.format(heading=heading))
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000426
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000427 def match(self, author):
428 if '@' in self.user:
429 return author == self.user
430 return author.startswith(self.user + '@')
431
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000432 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000433 activity = len([
434 reply
435 for reply in change['replies']
436 if self.match(reply['author'])
437 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000438 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000439 'created': change['created'].date().isoformat(),
440 'modified': change['modified'].date().isoformat(),
441 'reviewers': ', '.join(change['reviewers']),
442 'status': change['status'],
443 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000444 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000445 if self.options.deltas:
446 optional_values['delta'] = change['delta']
447
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000448 self.print_generic(self.options.output_format,
449 self.options.output_format_changes,
450 change['header'],
451 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000452 change['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000453 change['created'],
454 change['modified'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000455 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000456
457 def print_issue(self, issue):
458 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000459 'created': issue['created'].date().isoformat(),
460 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000461 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000462 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000463 }
464 self.print_generic(self.options.output_format,
465 self.options.output_format_issues,
466 issue['header'],
467 issue['url'],
468 issue['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000469 issue['created'],
470 issue['modified'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000471 optional_values)
472
473 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000474 activity = len([
475 reply
476 for reply in review['replies']
477 if self.match(reply['author'])
478 ])
479 optional_values = {
480 'created': review['created'].date().isoformat(),
481 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800482 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000483 'activity': activity,
484 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800485 if self.options.deltas:
486 optional_values['delta'] = review['delta']
487
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000488 self.print_generic(self.options.output_format,
489 self.options.output_format_reviews,
490 review['header'],
491 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000492 review['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000493 review['created'],
494 review['modified'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000495 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000496
497 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000498 def print_generic(default_fmt, specific_fmt,
Lutz Justen860378e2019-03-12 01:10:02 +0000499 title, url, author, created, modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000500 optional_values=None):
501 output_format = specific_fmt if specific_fmt is not None else default_fmt
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000502 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000503 'title': title,
504 'url': url,
505 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000506 'created': created,
507 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000508 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000509 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000510 values.update(optional_values)
Edward Lemur2a048032020-01-14 22:58:13 +0000511 print(DefaultFormatter().format(output_format, **values))
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000512
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000513
514 def filter_issue(self, issue, should_filter_by_user=True):
515 def maybe_filter_username(email):
516 return not should_filter_by_user or username(email) == self.user
517 if (maybe_filter_username(issue['author']) and
518 self.filter_modified(issue['created'])):
519 return True
520 if (maybe_filter_username(issue['owner']) and
521 (self.filter_modified(issue['created']) or
522 self.filter_modified(issue['modified']))):
523 return True
524 for reply in issue['replies']:
525 if self.filter_modified(reply['created']):
526 if not should_filter_by_user:
527 break
528 if (username(reply['author']) == self.user
529 or (self.user + '@') in reply['content']):
530 break
531 else:
532 return False
533 return True
534
535 def filter_modified(self, modified):
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000536 return self.modified_after < modified < self.modified_before
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000537
538 def auth_for_changes(self):
539 #TODO(cjhopman): Move authentication check for getting changes here.
540 pass
541
542 def auth_for_reviews(self):
543 # Reviews use all the same instances as changes so no authentication is
544 # required.
545 pass
546
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000547 def get_changes(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000548 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100549 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100550 gerrit_changes = pool.map_async(
551 lambda instance: self.gerrit_search(instance, owner=self.user),
552 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100553 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000554 self.changes = list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000555
556 def print_changes(self):
557 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000558 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000559 for change in self.changes:
Bruce Dawson2afcf222019-02-25 22:07:08 +0000560 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000561
Vadim Bendebury8de38002018-05-14 19:02:55 -0700562 def print_access_errors(self):
563 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400564 logging.error('Access Errors:')
565 for error in self.access_errors:
566 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700567
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000568 def get_reviews(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000569 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100570 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100571 gerrit_reviews = pool.map_async(
572 lambda instance: self.gerrit_search(instance, reviewer=self.user),
573 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100574 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000575 self.reviews = list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000576
577 def print_reviews(self):
578 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000579 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000580 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000581 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000582
583 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100584 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
585 monorail_issues = pool.map(
586 self.monorail_issue_search, monorail_projects.keys())
587 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
588
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700589 if not monorail_issues:
590 return
591
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100592 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
593 filtered_issues = pool.map(
594 self.filter_modified_monorail_issue, monorail_issues)
595 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100596
597 def get_referenced_issues(self):
598 if not self.issues:
599 self.get_issues()
600
601 if not self.changes:
602 self.get_changes()
603
604 referenced_issue_uids = set(itertools.chain.from_iterable(
605 change['bugs'] for change in self.changes))
606 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
607 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
608
609 missing_issues_by_project = collections.defaultdict(list)
610 for issue_uid in missing_issue_uids:
611 project, issue_id = issue_uid.split(':')
612 missing_issues_by_project[project].append(issue_id)
613
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000614 for project, issue_ids in missing_issues_by_project.items():
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100615 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000616
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000617 def print_issues(self):
618 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000619 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000620 for issue in self.issues:
621 self.print_issue(issue)
622
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100623 def print_changes_by_issue(self, skip_empty_own):
624 if not self.issues or not self.changes:
625 return
626
627 self.print_heading('Changes by referenced issue(s)')
628 issues = {issue['uid']: issue for issue in self.issues}
629 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
630 changes_by_issue_uid = collections.defaultdict(list)
631 changes_by_ref_issue_uid = collections.defaultdict(list)
632 changes_without_issue = []
633 for change in self.changes:
634 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000635 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100636 if issue_uid in issues:
637 changes_by_issue_uid[issue_uid].append(change)
638 added = True
639 if issue_uid in ref_issues:
640 changes_by_ref_issue_uid[issue_uid].append(change)
641 added = True
642 if not added:
643 changes_without_issue.append(change)
644
645 # Changes referencing own issues.
646 for issue_uid in issues:
647 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
648 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000649 if changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000650 print()
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000651 for change in changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000652 print(' ', end='') # this prints no newline
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000653 self.print_change(change)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000654 print()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100655
656 # Changes referencing others' issues.
657 for issue_uid in ref_issues:
658 assert changes_by_ref_issue_uid[issue_uid]
659 self.print_issue(ref_issues[issue_uid])
660 for change in changes_by_ref_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000661 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100662 self.print_change(change)
663
664 # Changes referencing no issues.
665 if changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000666 print(self.options.output_format_no_url.format(title='Other changes'))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100667 for change in changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000668 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100669 self.print_change(change)
670
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000671 def print_activity(self):
672 self.print_changes()
673 self.print_reviews()
674 self.print_issues()
675
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000676 def dump_json(self, ignore_keys=None):
677 if ignore_keys is None:
678 ignore_keys = ['replies']
679
680 def format_for_json_dump(in_array):
681 output = {}
682 for item in in_array:
683 url = item.get('url') or item.get('review_url')
684 if not url:
685 raise Exception('Dumped item %s does not specify url' % item)
686 output[url] = dict(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000687 (k, v) for k,v in item.items() if k not in ignore_keys)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000688 return output
689
690 class PythonObjectEncoder(json.JSONEncoder):
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000691 def default(self, o): # pylint: disable=method-hidden
692 if isinstance(o, datetime):
693 return o.isoformat()
694 if isinstance(o, set):
695 return list(o)
696 return json.JSONEncoder.default(self, o)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000697
698 output = {
699 'reviews': format_for_json_dump(self.reviews),
700 'changes': format_for_json_dump(self.changes),
701 'issues': format_for_json_dump(self.issues)
702 }
Raul Tambre80ee78e2019-05-06 22:41:05 +0000703 print(json.dumps(output, indent=2, cls=PythonObjectEncoder))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000704
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000705
706def main():
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000707 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
708 parser.add_option(
709 '-u', '--user', metavar='<email>',
Bruce Dawson84370962019-02-21 05:33:37 +0000710 # Look for USER and USERNAME (Windows) environment variables.
711 default=os.environ.get('USER', os.environ.get('USERNAME')),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000712 help='Filter on user, default=%default')
713 parser.add_option(
714 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000715 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000716 parser.add_option(
717 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000718 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000719 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
720 relativedelta(months=2))
721 parser.add_option(
722 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000723 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000724 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
725 parser.add_option(
726 '-Y', '--this_year', action='store_true',
727 help='Use this year\'s dates')
728 parser.add_option(
729 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000730 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000731 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000732 '-W', '--last_week', action='count',
733 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000734 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000735 '-a', '--auth',
736 action='store_true',
737 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000738 parser.add_option(
739 '-d', '--deltas',
740 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800741 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100742 parser.add_option(
743 '--no-referenced-issues',
744 action='store_true',
745 help='Do not fetch issues referenced by owned changes. Useful in '
746 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100747 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100748 parser.add_option(
Vadim Bendebury80012972019-11-23 02:23:11 +0000749 '--skip_servers',
750 action='store',
751 default='',
752 help='A comma separated list of gerrit and rietveld servers to ignore')
753 parser.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100754 '--skip-own-issues-without-changes',
755 action='store_true',
756 help='Skips listing own issues without changes when showing changes '
757 'grouped by referenced issue(s). See --changes-by-issue for more '
758 'details.')
Nicolas Boichatcd3696c2021-06-02 01:42:18 +0000759 parser.add_option(
760 '-F', '--config_file', metavar='<config_file>',
761 help='Configuration file in JSON format, used to add additional gerrit '
762 'instances (see source code for an example).')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000763
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000764 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000765 'By default, all activity will be looked up and '
766 'printed. If any of these are specified, only '
767 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000768 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000769 '-c', '--changes',
770 action='store_true',
771 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000772 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000773 '-i', '--issues',
774 action='store_true',
775 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000776 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000777 '-r', '--reviews',
778 action='store_true',
779 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100780 activity_types_group.add_option(
781 '--changes-by-issue', action='store_true',
782 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000783 parser.add_option_group(activity_types_group)
784
785 output_format_group = optparse.OptionGroup(parser, 'Output Format',
786 'By default, all activity will be printed in the '
787 'following format: {url} {title}. This can be '
788 'changed for either all activity types or '
789 'individually for each activity type. The format '
790 'is defined as documented for '
791 'string.format(...). The variables available for '
Lutz Justen860378e2019-03-12 01:10:02 +0000792 'all activity types are url, title, author, '
793 'created and modified. Format options for '
794 'specific activity types will override the '
795 'generic format.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000796 output_format_group.add_option(
797 '-f', '--output-format', metavar='<format>',
798 default=u'{url} {title}',
799 help='Specifies the format to use when printing all your activity.')
800 output_format_group.add_option(
801 '--output-format-changes', metavar='<format>',
802 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000803 help='Specifies the format to use when printing changes. Supports the '
804 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000805 output_format_group.add_option(
806 '--output-format-issues', metavar='<format>',
807 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000808 help='Specifies the format to use when printing issues. Supports the '
809 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000810 output_format_group.add_option(
811 '--output-format-reviews', metavar='<format>',
812 default=None,
813 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000814 output_format_group.add_option(
815 '--output-format-heading', metavar='<format>',
816 default=u'{heading}:',
Vincent Scheib2be61a12020-05-26 16:45:32 +0000817 help='Specifies the format to use when printing headings. '
818 'Supports the variable {heading}.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000819 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100820 '--output-format-no-url', default='{title}',
821 help='Specifies the format to use when printing activity without url.')
822 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000823 '-m', '--markdown', action='store_true',
824 help='Use markdown-friendly output (overrides --output-format '
825 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000826 output_format_group.add_option(
827 '-j', '--json', action='store_true',
828 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000829 parser.add_option_group(output_format_group)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000830
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000831 parser.add_option(
832 '-v', '--verbose',
833 action='store_const',
834 dest='verbosity',
835 default=logging.WARN,
836 const=logging.INFO,
837 help='Output extra informational messages.'
838 )
839 parser.add_option(
840 '-q', '--quiet',
841 action='store_const',
842 dest='verbosity',
843 const=logging.ERROR,
844 help='Suppress non-error messages.'
845 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000846 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000847 '-M', '--merged-only',
848 action='store_true',
849 dest='merged_only',
850 default=False,
851 help='Shows only changes that have been merged.')
852 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000853 '-C', '--completed-issues',
854 action='store_true',
855 dest='completed_issues',
856 default=False,
857 help='Shows only monorail issues that have completed (Fixed|Verified) '
858 'by the user.')
859 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000860 '-o', '--output', metavar='<file>',
861 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000862
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000863 # Remove description formatting
864 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800865 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000866
867 options, args = parser.parse_args()
868 options.local_user = os.environ.get('USER')
869 if args:
870 parser.error('Args unsupported')
871 if not options.user:
Bruce Dawson84370962019-02-21 05:33:37 +0000872 parser.error('USER/USERNAME is not set, please use -u')
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000873 # Retains the original -u option as the email address.
874 options.email = options.user
875 options.user = username(options.email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000876
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000877 logging.basicConfig(level=options.verbosity)
878
879 # python-keyring provides easy access to the system keyring.
880 try:
881 import keyring # pylint: disable=unused-import,unused-variable,F0401
882 except ImportError:
883 logging.warning('Consider installing python-keyring')
884
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000885 if not options.begin:
886 if options.last_quarter:
887 begin, end = quarter_begin, quarter_end
888 elif options.this_year:
889 begin, end = get_year_of(datetime.today())
890 elif options.week_of:
891 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000892 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000893 begin, end = (get_week_of(datetime.today() -
894 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000895 else:
896 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
897 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700898 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000899 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700900 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000901 else:
902 end = datetime.today()
903 options.begin, options.end = begin, end
Bruce Dawson2afcf222019-02-25 22:07:08 +0000904 if begin >= end:
905 # The queries fail in peculiar ways when the begin date is in the future.
906 # Give a descriptive error message instead.
907 logging.error('Start date (%s) is the same or later than end date (%s)' %
908 (begin, end))
909 return 1
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000910
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000911 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000912 options.output_format_heading = '### {heading}\n'
913 options.output_format = ' * [{title}]({url})'
914 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000915 logging.info('Searching for activity by %s', options.user)
916 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000917
Nicolas Boichatcd3696c2021-06-02 01:42:18 +0000918 if options.config_file:
919 with open(options.config_file) as f:
920 config = json.load(f)
921
922 for item, entries in config.items():
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000923 if item == 'gerrit_instances':
924 for repo, dic in entries.items():
925 # Use property name as URL
926 dic['url'] = repo
927 gerrit_instances.append(dic)
928 elif item == 'monorail_projects':
929 monorail_projects.append(entries)
930 else:
931 logging.error('Invalid entry in config file.')
932 return 1
Nicolas Boichatcd3696c2021-06-02 01:42:18 +0000933
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000934 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100935 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000936
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100937 if not (options.changes or options.reviews or options.issues or
938 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000939 options.changes = True
940 options.issues = True
941 options.reviews = True
942
943 # First do any required authentication so none of the user interaction has to
944 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100945 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000946 my_activity.auth_for_changes()
947 if options.reviews:
948 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000949
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000950 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000951
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000952 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100953 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000954 my_activity.get_changes()
955 if options.reviews:
956 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100957 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000958 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100959 if not options.no_referenced_issues:
960 my_activity.get_referenced_issues()
Edward Lemur5b929a42019-10-21 17:57:39 +0000961 except auth.LoginRequiredError as e:
962 logging.error('auth.LoginRequiredError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000963
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100964 my_activity.show_progress('\n')
965
Vadim Bendebury8de38002018-05-14 19:02:55 -0700966 my_activity.print_access_errors()
967
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000968 output_file = None
969 try:
970 if options.output:
971 output_file = open(options.output, 'w')
972 logging.info('Printing output to "%s"', options.output)
973 sys.stdout = output_file
974 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700975 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000976 else:
977 if options.json:
978 my_activity.dump_json()
979 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100980 if options.changes:
981 my_activity.print_changes()
982 if options.reviews:
983 my_activity.print_reviews()
984 if options.issues:
985 my_activity.print_issues()
986 if options.changes_by_issue:
987 my_activity.print_changes_by_issue(
988 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000989 finally:
990 if output_file:
991 logging.info('Done printing to file.')
992 sys.stdout = sys.__stdout__
993 output_file.close()
994
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000995 return 0
996
997
998if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000999 # Fix encoding to support non-ascii issue titles.
1000 fix_encoding.fix_encoding()
1001
sbc@chromium.org013731e2015-02-26 18:28:43 +00001002 try:
1003 sys.exit(main())
1004 except KeyboardInterrupt:
1005 sys.stderr.write('interrupted\n')
1006 sys.exit(1)