blob: 8db0b13f69081b9fb98f6decb5052779c95d42eb [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
Tobias Sargeantffb3c432017-03-08 14:09:14 +000049from string import Formatter
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000050import sys
51import urllib
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +000052import re
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000053
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000054import auth
stevefung@chromium.org832d51e2015-05-27 12:52:51 +000055import fix_encoding
Edward Lesmesae3586b2020-03-23 21:21:14 +000056import gclient_utils
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000057import gerrit_util
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000058
Edward Lemur2a048032020-01-14 22:58:13 +000059if sys.version_info.major == 2:
Josip Sokcevic4940cc42021-10-05 23:55:34 +000060 logging.critical(
61 'Python 2 is not supported. Run my_activity.py using vpython3.')
62
Edward Lemur2a048032020-01-14 22:58:13 +000063
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000064try:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000065 import dateutil # pylint: disable=import-error
66 import dateutil.parser
67 from dateutil.relativedelta import relativedelta
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000068except ImportError:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000069 logging.error('python-dateutil package required')
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +000070 sys.exit(1)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000071
Tobias Sargeantffb3c432017-03-08 14:09:14 +000072
73class DefaultFormatter(Formatter):
74 def __init__(self, default = ''):
75 super(DefaultFormatter, self).__init__()
76 self.default = default
77
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +000078 def get_value(self, key, args, kwargs):
79 if isinstance(key, str) and key not in kwargs:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000080 return self.default
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +000081 return Formatter.get_value(self, key, args, kwargs)
82
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000083
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000084gerrit_instances = [
85 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000086 'url': 'android-review.googlesource.com',
Mike Frysingerfdf027a2020-02-27 21:53:45 +000087 'shorturl': 'r.android.com',
88 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000089 },
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000090 {
Mike Frysinger24fd8632020-02-27 22:07:06 +000091 'url': 'gerrit-review.googlesource.com',
92 },
93 {
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000094 'url': 'chrome-internal-review.googlesource.com',
Jeremy Romanf475a472017-06-06 16:49:11 -040095 'shorturl': 'crrev.com/i',
Varun Khanejad9f97bc2017-08-02 12:55:01 -070096 'short_url_protocol': 'https',
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000097 },
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000098 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000099 'url': 'chromium-review.googlesource.com',
100 'shorturl': 'crrev.com/c',
101 'short_url_protocol': 'https',
deymo@chromium.org56dc57a2015-09-10 18:26:54 +0000102 },
Ryan Harrison897602a2017-09-18 16:23:41 -0400103 {
Ryan Harrison06e18692019-09-23 18:22:25 +0000104 'url': 'dawn-review.googlesource.com',
105 },
106 {
Ryan Harrison897602a2017-09-18 16:23:41 -0400107 'url': 'pdfium-review.googlesource.com',
108 },
Adrienne Walker95d4c852018-09-27 20:28:12 +0000109 {
110 'url': 'skia-review.googlesource.com',
111 },
Paul Fagerburgb93d82c2020-08-17 16:19:46 +0000112 {
113 'url': 'review.coreboot.org',
114 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000115]
116
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100117monorail_projects = {
Jamie Madill1db68ea2019-09-03 19:34:42 +0000118 'angleproject': {
119 'shorturl': 'anglebug.com',
120 'short_url_protocol': 'http',
121 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100122 'chromium': {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000123 'shorturl': 'crbug.com',
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700124 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000125 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000126 'dawn': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100127 'google-breakpad': {},
128 'gyp': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100129 'pdfium': {
Ryan Harrison897602a2017-09-18 16:23:41 -0400130 'shorturl': 'crbug.com/pdfium',
131 'short_url_protocol': 'https',
132 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000133 'skia': {},
Ryan Harrison97811152021-03-29 20:30:57 +0000134 'tint': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100135 'v8': {
136 'shorturl': 'crbug.com/v8',
137 'short_url_protocol': 'https',
138 },
139}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000140
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000141def username(email):
142 """Keeps the username of an email address."""
143 return email and email.split('@', 1)[0]
144
145
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000146def datetime_to_midnight(date):
147 return date - timedelta(hours=date.hour, minutes=date.minute,
148 seconds=date.second, microseconds=date.microsecond)
149
150
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000151def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000152 begin = (datetime_to_midnight(date) -
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000153 relativedelta(months=(date.month - 1) % 3, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000154 return begin, begin + relativedelta(months=3)
155
156
157def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000158 begin = (datetime_to_midnight(date) -
159 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000160 return begin, begin + relativedelta(years=1)
161
162
163def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000164 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000165 return begin, begin + timedelta(days=7)
166
167
168def get_yes_or_no(msg):
169 while True:
Edward Lesmesae3586b2020-03-23 21:21:14 +0000170 response = gclient_utils.AskForData(msg + ' yes/no [no] ')
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000171 if response in ('y', 'yes'):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000172 return True
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000173
174 if not response or response in ('n', 'no'):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000175 return False
176
177
deymo@chromium.org6c039202013-09-12 12:28:12 +0000178def datetime_from_gerrit(date_string):
179 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
180
181
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100182def datetime_from_monorail(date_string):
183 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000184
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000185def extract_bug_numbers_from_description(issue):
186 # Getting the description for REST Gerrit
187 revision = issue['revisions'][issue['current_revision']]
188 description = revision['commit']['message']
189
190 bugs = []
191 # Handle both "Bug: 99999" and "BUG=99999" bug notations
192 # Multiple bugs can be noted on a single line or in multiple ones.
193 matches = re.findall(
194 r'^(BUG=|(Bug|Fixed):\s*)((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)',
195 description, flags=re.IGNORECASE | re.MULTILINE)
196 if matches:
197 for match in matches:
198 bugs.extend(match[2].replace(' ', '').split(','))
199 # Add default chromium: prefix if none specified.
200 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
201
202 return sorted(set(bugs))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000203
204class MyActivity(object):
205 def __init__(self, options):
206 self.options = options
207 self.modified_after = options.begin
208 self.modified_before = options.end
209 self.user = options.user
210 self.changes = []
211 self.reviews = []
212 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100213 self.referenced_issues = []
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000214 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-14 19:02:55 -0700215 self.access_errors = set()
Vadim Bendebury80012972019-11-23 02:23:11 +0000216 self.skip_servers = (options.skip_servers.split(','))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000217
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100218 def show_progress(self, how='.'):
219 if sys.stdout.isatty():
220 sys.stdout.write(how)
221 sys.stdout.flush()
222
Vadim Bendebury8de38002018-05-14 19:02:55 -0700223 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200224 # Convert the "key:value" filter to a list of (key, value) pairs.
225 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000226 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000227 # Instantiate the generator to force all the requests now and catch the
228 # errors here.
229 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000230 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
231 'CURRENT_REVISION', 'CURRENT_COMMIT']))
Raul Tambre7c938462019-05-24 16:35:35 +0000232 except gerrit_util.GerritError as e:
Vadim Bendebury8de38002018-05-14 19:02:55 -0700233 error_message = 'Looking up %r: %s' % (instance['url'], e)
234 if error_message not in self.access_errors:
235 self.access_errors.add(error_message)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000236 return []
237
deymo@chromium.org6c039202013-09-12 12:28:12 +0000238 def gerrit_search(self, instance, owner=None, reviewer=None):
Vadim Bendebury80012972019-11-23 02:23:11 +0000239 if instance['url'] in self.skip_servers:
240 return []
deymo@chromium.org6c039202013-09-12 12:28:12 +0000241 max_age = datetime.today() - self.modified_after
Andrii Shyshkalov4aedec02018-09-17 16:31:17 +0000242 filters = ['-age:%ss' % (max_age.days * 24 * 3600 + max_age.seconds)]
243 if owner:
244 assert not reviewer
245 filters.append('owner:%s' % owner)
246 else:
247 filters.extend(('-owner:%s' % reviewer, 'reviewer:%s' % reviewer))
Peter K. Lee9342ac02018-08-28 21:03:51 +0000248 # TODO(cjhopman): Should abandoned changes be filtered out when
249 # merged_only is not enabled?
250 if self.options.merged_only:
251 filters.append('status:merged')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000252
Aaron Gable2979a872017-09-05 17:38:32 -0700253 issues = self.gerrit_changes_over_rest(instance, filters)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100254 self.show_progress()
Aaron Gable2979a872017-09-05 17:38:32 -0700255 issues = [self.process_gerrit_issue(instance, issue)
256 for issue in issues]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000257
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000258 issues = filter(self.filter_issue, issues)
259 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
260
261 return issues
262
Aaron Gable2979a872017-09-05 17:38:32 -0700263 def process_gerrit_issue(self, instance, issue):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000264 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000265 if self.options.deltas:
266 ret['delta'] = DefaultFormatter().format(
267 '+{insertions},-{deletions}',
268 **issue)
269 ret['status'] = issue['status']
deymo@chromium.org6c039202013-09-12 12:28:12 +0000270 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700271 protocol = instance.get('short_url_protocol', 'http')
272 url = instance['shorturl']
273 else:
274 protocol = 'https'
275 url = instance['url']
276 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
277
deymo@chromium.org6c039202013-09-12 12:28:12 +0000278 ret['header'] = issue['subject']
Don Garrett2ebf9fd2018-08-06 15:14:00 +0000279 ret['owner'] = issue['owner'].get('email', '')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000280 ret['author'] = ret['owner']
281 ret['created'] = datetime_from_gerrit(issue['created'])
282 ret['modified'] = datetime_from_gerrit(issue['updated'])
283 if 'messages' in issue:
Aaron Gable2979a872017-09-05 17:38:32 -0700284 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
deymo@chromium.org6c039202013-09-12 12:28:12 +0000285 else:
286 ret['replies'] = []
287 ret['reviewers'] = set(r['author'] for r in ret['replies'])
288 ret['reviewers'].discard(ret['author'])
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000289 ret['bugs'] = extract_bug_numbers_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000290 return ret
291
292 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700293 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000294 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000295 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
296 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000297 for reply in replies:
298 ret.append({
299 'author': reply['author']['email'],
300 'created': datetime_from_gerrit(reply['date']),
301 'content': reply['message'],
302 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000303 return ret
304
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100305 def monorail_get_auth_http(self):
Kenneth Russell66badbd2018-09-09 21:35:32 +0000306 # Manually use a long timeout (10m); for some users who have a
307 # long history on the issue tracker, whatever the default timeout
308 # is is reached.
Edward Lemur5b929a42019-10-21 17:57:39 +0000309 return auth.Authenticator().authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100310
311 def filter_modified_monorail_issue(self, issue):
312 """Precisely checks if an issue has been modified in the time range.
313
314 This fetches all issue comments to check if the issue has been modified in
315 the time range specified by user. This is needed because monorail only
316 allows filtering by last updated and published dates, which is not
317 sufficient to tell whether a given issue has been modified at some specific
318 time range. Any update to the issue is a reported as comment on Monorail.
319
320 Args:
321 issue: Issue dict as returned by monorail_query_issues method. In
322 particular, must have a key 'uid' formatted as 'project:issue_id'.
323
324 Returns:
325 Passed issue if modified, None otherwise.
326 """
327 http = self.monorail_get_auth_http()
328 project, issue_id = issue['uid'].split(':')
329 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
330 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
331 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100332 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100333 content = json.loads(body)
334 if not content:
335 logging.error('Unable to parse %s response from monorail.', project)
336 return issue
337
338 for item in content.get('items', []):
339 comment_published = datetime_from_monorail(item['published'])
340 if self.filter_modified(comment_published):
341 return issue
342
343 return None
344
345 def monorail_query_issues(self, project, query):
346 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000347 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100348 '/%s/issues') % project
Josip Sokcevic4940cc42021-10-05 23:55:34 +0000349 query_data = urllib.parse.urlencode(query)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100350 url = url + '?' + query_data
351 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100352 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100353 content = json.loads(body)
354 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100355 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100356 return []
357
358 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100359 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100360 for item in content.get('items', []):
361 if project_config.get('shorturl'):
362 protocol = project_config.get('short_url_protocol', 'http')
363 item_url = '%s://%s/%d' % (
364 protocol, project_config['shorturl'], item['id'])
365 else:
366 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
367 project, item['id'])
368 issue = {
369 'uid': '%s:%s' % (project, item['id']),
370 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100371 'created': datetime_from_monorail(item['published']),
372 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100373 'author': item['author']['name'],
374 'url': item_url,
375 'comments': [],
376 'status': item['status'],
377 'labels': [],
378 'components': []
379 }
380 if 'owner' in item:
381 issue['owner'] = item['owner']['name']
382 else:
383 issue['owner'] = 'None'
384 if 'labels' in item:
385 issue['labels'] = item['labels']
386 if 'components' in item:
387 issue['components'] = item['components']
388 issues.append(issue)
389
390 return issues
391
392 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000393 epoch = datetime.utcfromtimestamp(0)
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000394 # Defaults to @chromium.org email if one wasn't provided on -u option.
395 user_str = (self.options.email if self.options.email.find('@') >= 0
396 else '%s@chromium.org' % self.user)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000397
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100398 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000399 'maxResults': 10000,
400 'q': user_str,
401 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
402 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000403 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000404
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000405 if self.options.completed_issues:
406 return [
407 issue for issue in issues
408 if (self.match(issue['owner']) and
409 issue['status'].lower() in ('verified', 'fixed'))
410 ]
411
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100412 return [
413 issue for issue in issues
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000414 if user_str in (issue['author'], issue['owner'])]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000415
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100416 def monorail_get_issues(self, project, issue_ids):
417 return self.monorail_query_issues(project, {
418 'maxResults': 10000,
419 'q': 'id:%s' % ','.join(issue_ids)
420 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000421
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000422 def print_heading(self, heading):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000423 print()
424 print(self.options.output_format_heading.format(heading=heading))
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000425
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000426 def match(self, author):
427 if '@' in self.user:
428 return author == self.user
429 return author.startswith(self.user + '@')
430
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000431 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000432 activity = len([
433 reply
434 for reply in change['replies']
435 if self.match(reply['author'])
436 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000437 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000438 'created': change['created'].date().isoformat(),
439 'modified': change['modified'].date().isoformat(),
440 'reviewers': ', '.join(change['reviewers']),
441 'status': change['status'],
442 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000443 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000444 if self.options.deltas:
445 optional_values['delta'] = change['delta']
446
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000447 self.print_generic(self.options.output_format,
448 self.options.output_format_changes,
449 change['header'],
450 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000451 change['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000452 change['created'],
453 change['modified'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000454 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000455
456 def print_issue(self, issue):
457 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000458 'created': issue['created'].date().isoformat(),
459 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000460 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000461 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000462 }
463 self.print_generic(self.options.output_format,
464 self.options.output_format_issues,
465 issue['header'],
466 issue['url'],
467 issue['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000468 issue['created'],
469 issue['modified'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000470 optional_values)
471
472 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000473 activity = len([
474 reply
475 for reply in review['replies']
476 if self.match(reply['author'])
477 ])
478 optional_values = {
479 'created': review['created'].date().isoformat(),
480 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800481 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000482 'activity': activity,
483 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800484 if self.options.deltas:
485 optional_values['delta'] = review['delta']
486
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000487 self.print_generic(self.options.output_format,
488 self.options.output_format_reviews,
489 review['header'],
490 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000491 review['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000492 review['created'],
493 review['modified'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000494 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000495
496 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000497 def print_generic(default_fmt, specific_fmt,
Lutz Justen860378e2019-03-12 01:10:02 +0000498 title, url, author, created, modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000499 optional_values=None):
500 output_format = specific_fmt if specific_fmt is not None else default_fmt
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000501 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000502 'title': title,
503 'url': url,
504 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000505 'created': created,
506 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000507 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000508 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000509 values.update(optional_values)
Edward Lemur2a048032020-01-14 22:58:13 +0000510 print(DefaultFormatter().format(output_format, **values))
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000511
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000512
513 def filter_issue(self, issue, should_filter_by_user=True):
514 def maybe_filter_username(email):
515 return not should_filter_by_user or username(email) == self.user
516 if (maybe_filter_username(issue['author']) and
517 self.filter_modified(issue['created'])):
518 return True
519 if (maybe_filter_username(issue['owner']) and
520 (self.filter_modified(issue['created']) or
521 self.filter_modified(issue['modified']))):
522 return True
523 for reply in issue['replies']:
524 if self.filter_modified(reply['created']):
525 if not should_filter_by_user:
526 break
527 if (username(reply['author']) == self.user
528 or (self.user + '@') in reply['content']):
529 break
530 else:
531 return False
532 return True
533
534 def filter_modified(self, modified):
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000535 return self.modified_after < modified < self.modified_before
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000536
537 def auth_for_changes(self):
538 #TODO(cjhopman): Move authentication check for getting changes here.
539 pass
540
541 def auth_for_reviews(self):
542 # Reviews use all the same instances as changes so no authentication is
543 # required.
544 pass
545
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000546 def get_changes(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000547 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100548 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100549 gerrit_changes = pool.map_async(
550 lambda instance: self.gerrit_search(instance, owner=self.user),
551 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100552 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000553 self.changes = list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000554
555 def print_changes(self):
556 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000557 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000558 for change in self.changes:
Bruce Dawson2afcf222019-02-25 22:07:08 +0000559 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000560
Vadim Bendebury8de38002018-05-14 19:02:55 -0700561 def print_access_errors(self):
562 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400563 logging.error('Access Errors:')
564 for error in self.access_errors:
565 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700566
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000567 def get_reviews(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000568 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100569 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100570 gerrit_reviews = pool.map_async(
571 lambda instance: self.gerrit_search(instance, reviewer=self.user),
572 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100573 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000574 self.reviews = list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000575
576 def print_reviews(self):
577 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000578 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000579 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000580 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000581
582 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100583 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
584 monorail_issues = pool.map(
585 self.monorail_issue_search, monorail_projects.keys())
586 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
587
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700588 if not monorail_issues:
589 return
590
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100591 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
592 filtered_issues = pool.map(
593 self.filter_modified_monorail_issue, monorail_issues)
594 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100595
596 def get_referenced_issues(self):
597 if not self.issues:
598 self.get_issues()
599
600 if not self.changes:
601 self.get_changes()
602
603 referenced_issue_uids = set(itertools.chain.from_iterable(
604 change['bugs'] for change in self.changes))
605 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
606 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
607
608 missing_issues_by_project = collections.defaultdict(list)
609 for issue_uid in missing_issue_uids:
610 project, issue_id = issue_uid.split(':')
611 missing_issues_by_project[project].append(issue_id)
612
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000613 for project, issue_ids in missing_issues_by_project.items():
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100614 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000615
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000616 def print_issues(self):
617 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000618 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000619 for issue in self.issues:
620 self.print_issue(issue)
621
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100622 def print_changes_by_issue(self, skip_empty_own):
623 if not self.issues or not self.changes:
624 return
625
626 self.print_heading('Changes by referenced issue(s)')
627 issues = {issue['uid']: issue for issue in self.issues}
628 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
629 changes_by_issue_uid = collections.defaultdict(list)
630 changes_by_ref_issue_uid = collections.defaultdict(list)
631 changes_without_issue = []
632 for change in self.changes:
633 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000634 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100635 if issue_uid in issues:
636 changes_by_issue_uid[issue_uid].append(change)
637 added = True
638 if issue_uid in ref_issues:
639 changes_by_ref_issue_uid[issue_uid].append(change)
640 added = True
641 if not added:
642 changes_without_issue.append(change)
643
644 # Changes referencing own issues.
645 for issue_uid in issues:
646 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
647 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000648 if changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000649 print()
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000650 for change in changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000651 print(' ', end='') # this prints no newline
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000652 self.print_change(change)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000653 print()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100654
655 # Changes referencing others' issues.
656 for issue_uid in ref_issues:
657 assert changes_by_ref_issue_uid[issue_uid]
658 self.print_issue(ref_issues[issue_uid])
659 for change in changes_by_ref_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000660 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100661 self.print_change(change)
662
663 # Changes referencing no issues.
664 if changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000665 print(self.options.output_format_no_url.format(title='Other changes'))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100666 for change in changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000667 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100668 self.print_change(change)
669
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000670 def print_activity(self):
671 self.print_changes()
672 self.print_reviews()
673 self.print_issues()
674
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000675 def dump_json(self, ignore_keys=None):
676 if ignore_keys is None:
677 ignore_keys = ['replies']
678
679 def format_for_json_dump(in_array):
680 output = {}
681 for item in in_array:
682 url = item.get('url') or item.get('review_url')
683 if not url:
684 raise Exception('Dumped item %s does not specify url' % item)
685 output[url] = dict(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000686 (k, v) for k,v in item.items() if k not in ignore_keys)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000687 return output
688
689 class PythonObjectEncoder(json.JSONEncoder):
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000690 def default(self, o): # pylint: disable=method-hidden
691 if isinstance(o, datetime):
692 return o.isoformat()
693 if isinstance(o, set):
694 return list(o)
695 return json.JSONEncoder.default(self, o)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000696
697 output = {
698 'reviews': format_for_json_dump(self.reviews),
699 'changes': format_for_json_dump(self.changes),
700 'issues': format_for_json_dump(self.issues)
701 }
Raul Tambre80ee78e2019-05-06 22:41:05 +0000702 print(json.dumps(output, indent=2, cls=PythonObjectEncoder))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000703
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000704
705def main():
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000706 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
707 parser.add_option(
708 '-u', '--user', metavar='<email>',
Bruce Dawson84370962019-02-21 05:33:37 +0000709 # Look for USER and USERNAME (Windows) environment variables.
710 default=os.environ.get('USER', os.environ.get('USERNAME')),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000711 help='Filter on user, default=%default')
712 parser.add_option(
713 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000714 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000715 parser.add_option(
716 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000717 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000718 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
719 relativedelta(months=2))
720 parser.add_option(
721 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000722 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000723 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
724 parser.add_option(
725 '-Y', '--this_year', action='store_true',
726 help='Use this year\'s dates')
727 parser.add_option(
728 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000729 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000730 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000731 '-W', '--last_week', action='count',
732 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000733 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000734 '-a', '--auth',
735 action='store_true',
736 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000737 parser.add_option(
738 '-d', '--deltas',
739 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800740 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100741 parser.add_option(
742 '--no-referenced-issues',
743 action='store_true',
744 help='Do not fetch issues referenced by owned changes. Useful in '
745 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100746 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100747 parser.add_option(
Vadim Bendebury80012972019-11-23 02:23:11 +0000748 '--skip_servers',
749 action='store',
750 default='',
751 help='A comma separated list of gerrit and rietveld servers to ignore')
752 parser.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100753 '--skip-own-issues-without-changes',
754 action='store_true',
755 help='Skips listing own issues without changes when showing changes '
756 'grouped by referenced issue(s). See --changes-by-issue for more '
757 'details.')
Nicolas Boichatcd3696c2021-06-02 01:42:18 +0000758 parser.add_option(
759 '-F', '--config_file', metavar='<config_file>',
760 help='Configuration file in JSON format, used to add additional gerrit '
761 'instances (see source code for an example).')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000762
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000763 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000764 'By default, all activity will be looked up and '
765 'printed. If any of these are specified, only '
766 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000767 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000768 '-c', '--changes',
769 action='store_true',
770 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000771 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000772 '-i', '--issues',
773 action='store_true',
774 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000775 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000776 '-r', '--reviews',
777 action='store_true',
778 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100779 activity_types_group.add_option(
780 '--changes-by-issue', action='store_true',
781 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000782 parser.add_option_group(activity_types_group)
783
784 output_format_group = optparse.OptionGroup(parser, 'Output Format',
785 'By default, all activity will be printed in the '
786 'following format: {url} {title}. This can be '
787 'changed for either all activity types or '
788 'individually for each activity type. The format '
789 'is defined as documented for '
790 'string.format(...). The variables available for '
Lutz Justen860378e2019-03-12 01:10:02 +0000791 'all activity types are url, title, author, '
792 'created and modified. Format options for '
793 'specific activity types will override the '
794 'generic format.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000795 output_format_group.add_option(
796 '-f', '--output-format', metavar='<format>',
797 default=u'{url} {title}',
798 help='Specifies the format to use when printing all your activity.')
799 output_format_group.add_option(
800 '--output-format-changes', metavar='<format>',
801 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000802 help='Specifies the format to use when printing changes. Supports the '
803 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000804 output_format_group.add_option(
805 '--output-format-issues', metavar='<format>',
806 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000807 help='Specifies the format to use when printing issues. Supports the '
808 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000809 output_format_group.add_option(
810 '--output-format-reviews', metavar='<format>',
811 default=None,
812 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000813 output_format_group.add_option(
814 '--output-format-heading', metavar='<format>',
815 default=u'{heading}:',
Vincent Scheib2be61a12020-05-26 16:45:32 +0000816 help='Specifies the format to use when printing headings. '
817 'Supports the variable {heading}.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000818 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100819 '--output-format-no-url', default='{title}',
820 help='Specifies the format to use when printing activity without url.')
821 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000822 '-m', '--markdown', action='store_true',
823 help='Use markdown-friendly output (overrides --output-format '
824 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000825 output_format_group.add_option(
826 '-j', '--json', action='store_true',
827 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000828 parser.add_option_group(output_format_group)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000829
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000830 parser.add_option(
831 '-v', '--verbose',
832 action='store_const',
833 dest='verbosity',
834 default=logging.WARN,
835 const=logging.INFO,
836 help='Output extra informational messages.'
837 )
838 parser.add_option(
839 '-q', '--quiet',
840 action='store_const',
841 dest='verbosity',
842 const=logging.ERROR,
843 help='Suppress non-error messages.'
844 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000845 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000846 '-M', '--merged-only',
847 action='store_true',
848 dest='merged_only',
849 default=False,
850 help='Shows only changes that have been merged.')
851 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000852 '-C', '--completed-issues',
853 action='store_true',
854 dest='completed_issues',
855 default=False,
856 help='Shows only monorail issues that have completed (Fixed|Verified) '
857 'by the user.')
858 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000859 '-o', '--output', metavar='<file>',
860 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000861
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000862 # Remove description formatting
863 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800864 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000865
866 options, args = parser.parse_args()
867 options.local_user = os.environ.get('USER')
868 if args:
869 parser.error('Args unsupported')
870 if not options.user:
Bruce Dawson84370962019-02-21 05:33:37 +0000871 parser.error('USER/USERNAME is not set, please use -u')
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000872 # Retains the original -u option as the email address.
873 options.email = options.user
874 options.user = username(options.email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000875
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000876 logging.basicConfig(level=options.verbosity)
877
878 # python-keyring provides easy access to the system keyring.
879 try:
880 import keyring # pylint: disable=unused-import,unused-variable,F0401
881 except ImportError:
882 logging.warning('Consider installing python-keyring')
883
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000884 if not options.begin:
885 if options.last_quarter:
886 begin, end = quarter_begin, quarter_end
887 elif options.this_year:
888 begin, end = get_year_of(datetime.today())
889 elif options.week_of:
890 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000891 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000892 begin, end = (get_week_of(datetime.today() -
893 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000894 else:
895 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
896 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700897 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000898 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700899 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000900 else:
901 end = datetime.today()
902 options.begin, options.end = begin, end
Bruce Dawson2afcf222019-02-25 22:07:08 +0000903 if begin >= end:
904 # The queries fail in peculiar ways when the begin date is in the future.
905 # Give a descriptive error message instead.
906 logging.error('Start date (%s) is the same or later than end date (%s)' %
907 (begin, end))
908 return 1
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000909
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000910 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000911 options.output_format_heading = '### {heading}\n'
912 options.output_format = ' * [{title}]({url})'
913 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000914 logging.info('Searching for activity by %s', options.user)
915 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000916
Nicolas Boichatcd3696c2021-06-02 01:42:18 +0000917 if options.config_file:
918 with open(options.config_file) as f:
919 config = json.load(f)
920
921 for item, entries in config.items():
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000922 if item == 'gerrit_instances':
923 for repo, dic in entries.items():
924 # Use property name as URL
925 dic['url'] = repo
926 gerrit_instances.append(dic)
927 elif item == 'monorail_projects':
928 monorail_projects.append(entries)
929 else:
930 logging.error('Invalid entry in config file.')
931 return 1
Nicolas Boichatcd3696c2021-06-02 01:42:18 +0000932
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000933 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100934 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000935
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100936 if not (options.changes or options.reviews or options.issues or
937 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000938 options.changes = True
939 options.issues = True
940 options.reviews = True
941
942 # First do any required authentication so none of the user interaction has to
943 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100944 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000945 my_activity.auth_for_changes()
946 if options.reviews:
947 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000948
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000949 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000950
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000951 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100952 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000953 my_activity.get_changes()
954 if options.reviews:
955 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100956 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000957 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100958 if not options.no_referenced_issues:
959 my_activity.get_referenced_issues()
Edward Lemur5b929a42019-10-21 17:57:39 +0000960 except auth.LoginRequiredError as e:
961 logging.error('auth.LoginRequiredError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000962
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100963 my_activity.show_progress('\n')
964
Vadim Bendebury8de38002018-05-14 19:02:55 -0700965 my_activity.print_access_errors()
966
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000967 output_file = None
968 try:
969 if options.output:
970 output_file = open(options.output, 'w')
971 logging.info('Printing output to "%s"', options.output)
972 sys.stdout = output_file
973 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700974 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000975 else:
976 if options.json:
977 my_activity.dump_json()
978 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100979 if options.changes:
980 my_activity.print_changes()
981 if options.reviews:
982 my_activity.print_reviews()
983 if options.issues:
984 my_activity.print_issues()
985 if options.changes_by_issue:
986 my_activity.print_changes_by_issue(
987 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000988 finally:
989 if output_file:
990 logging.info('Done printing to file.')
991 sys.stdout = sys.__stdout__
992 output_file.close()
993
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000994 return 0
995
996
997if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000998 # Fix encoding to support non-ascii issue titles.
999 fix_encoding.fix_encoding()
1000
sbc@chromium.org013731e2015-02-26 18:28:43 +00001001 try:
1002 sys.exit(main())
1003 except KeyboardInterrupt:
1004 sys.stderr.write('interrupted\n')
1005 sys.exit(1)