blob: ebac12641ce0467cca51492bd7b21b00f939cc15 [file] [log] [blame]
Gabriel Charettebc6617a2019-02-05 21:30:52 +00001#!/usr/bin/env vpython
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Get stats about your activity.
7
8Example:
9 - my_activity.py for stats for the current week (last week on mondays).
10 - my_activity.py -Q for stats for last quarter.
11 - my_activity.py -Y for stats for this year.
Mohamed Heikaldc37feb2019-06-28 22:33:58 +000012 - my_activity.py -b 4/24/19 for stats since April 24th 2019.
13 - my_activity.py -b 4/24/19 -e 6/16/19 stats between April 24th and June 16th.
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +000014 - my_activity.py -jd to output stats for the week to json with deltas data.
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000015"""
16
17# These services typically only provide a created time and a last modified time
18# for each item for general queries. This is not enough to determine if there
19# was activity in a given time period. So, we first query for all things created
20# before end and modified after begin. Then, we get the details of each item and
21# check those details to determine if there was activity in the given period.
22# This means that query time scales mostly with (today() - begin).
23
Raul Tambre80ee78e2019-05-06 22:41:05 +000024from __future__ import print_function
25
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010026import collections
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +010027import contextlib
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000028from datetime import datetime
29from datetime import timedelta
Edward Lemur202c5592019-10-21 22:44:52 +000030import httplib2
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010031import itertools
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000032import json
Tobias Sargeantffb3c432017-03-08 14:09:14 +000033import logging
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +010034from multiprocessing.pool import ThreadPool
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000035import optparse
36import os
37import subprocess
Tobias Sargeantffb3c432017-03-08 14:09:14 +000038from string import Formatter
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000039import sys
40import urllib
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +000041import re
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000042
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
stevefung@chromium.org832d51e2015-05-27 12:52:51 +000044import fix_encoding
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000045import gerrit_util
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000046
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +000047
Edward Lemur2a048032020-01-14 22:58:13 +000048if sys.version_info.major == 2:
49 import urllib as urllib_parse
50else:
51 import urllib.parse as urllib_parse
52
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000053try:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000054 import dateutil # pylint: disable=import-error
55 import dateutil.parser
56 from dateutil.relativedelta import relativedelta
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000057except ImportError:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000058 logging.error('python-dateutil package required')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000059 exit(1)
60
Tobias Sargeantffb3c432017-03-08 14:09:14 +000061
62class DefaultFormatter(Formatter):
63 def __init__(self, default = ''):
64 super(DefaultFormatter, self).__init__()
65 self.default = default
66
67 def get_value(self, key, args, kwds):
Edward Lemur2a048032020-01-14 22:58:13 +000068 if isinstance(key, str) and key not in kwds:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000069 return self.default
70 return Formatter.get_value(self, key, args, kwds)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000071
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000072gerrit_instances = [
73 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000074 'url': 'android-review.googlesource.com',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000075 },
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000076 {
77 'url': 'chrome-internal-review.googlesource.com',
Jeremy Romanf475a472017-06-06 16:49:11 -040078 'shorturl': 'crrev.com/i',
Varun Khanejad9f97bc2017-08-02 12:55:01 -070079 'short_url_protocol': 'https',
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000080 },
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000081 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000082 'url': 'chromium-review.googlesource.com',
83 'shorturl': 'crrev.com/c',
84 'short_url_protocol': 'https',
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000085 },
Ryan Harrison897602a2017-09-18 16:23:41 -040086 {
Ryan Harrison06e18692019-09-23 18:22:25 +000087 'url': 'dawn-review.googlesource.com',
88 },
89 {
Ryan Harrison897602a2017-09-18 16:23:41 -040090 'url': 'pdfium-review.googlesource.com',
91 },
Adrienne Walker95d4c852018-09-27 20:28:12 +000092 {
93 'url': 'skia-review.googlesource.com',
94 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000095]
96
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010097monorail_projects = {
Jamie Madill1db68ea2019-09-03 19:34:42 +000098 'angleproject': {
99 'shorturl': 'anglebug.com',
100 'short_url_protocol': 'http',
101 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100102 'chromium': {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000103 'shorturl': 'crbug.com',
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700104 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000105 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000106 'dawn': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100107 'google-breakpad': {},
108 'gyp': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100109 'pdfium': {
Ryan Harrison897602a2017-09-18 16:23:41 -0400110 'shorturl': 'crbug.com/pdfium',
111 'short_url_protocol': 'https',
112 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000113 'skia': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100114 'v8': {
115 'shorturl': 'crbug.com/v8',
116 'short_url_protocol': 'https',
117 },
118}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000119
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000120def username(email):
121 """Keeps the username of an email address."""
122 return email and email.split('@', 1)[0]
123
124
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000125def datetime_to_midnight(date):
126 return date - timedelta(hours=date.hour, minutes=date.minute,
127 seconds=date.second, microseconds=date.microsecond)
128
129
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000130def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000131 begin = (datetime_to_midnight(date) -
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000132 relativedelta(months=(date.month - 1) % 3, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000133 return begin, begin + relativedelta(months=3)
134
135
136def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000137 begin = (datetime_to_midnight(date) -
138 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000139 return begin, begin + relativedelta(years=1)
140
141
142def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000143 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000144 return begin, begin + timedelta(days=7)
145
146
147def get_yes_or_no(msg):
148 while True:
149 response = raw_input(msg + ' yes/no [no] ')
150 if response == 'y' or response == 'yes':
151 return True
152 elif not response or response == 'n' or response == 'no':
153 return False
154
155
deymo@chromium.org6c039202013-09-12 12:28:12 +0000156def datetime_from_gerrit(date_string):
157 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
158
159
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100160def datetime_from_monorail(date_string):
161 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000162
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000163def extract_bug_numbers_from_description(issue):
164 # Getting the description for REST Gerrit
165 revision = issue['revisions'][issue['current_revision']]
166 description = revision['commit']['message']
167
168 bugs = []
169 # Handle both "Bug: 99999" and "BUG=99999" bug notations
170 # Multiple bugs can be noted on a single line or in multiple ones.
171 matches = re.findall(
172 r'^(BUG=|(Bug|Fixed):\s*)((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)',
173 description, flags=re.IGNORECASE | re.MULTILINE)
174 if matches:
175 for match in matches:
176 bugs.extend(match[2].replace(' ', '').split(','))
177 # Add default chromium: prefix if none specified.
178 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
179
180 return sorted(set(bugs))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000181
182class MyActivity(object):
183 def __init__(self, options):
184 self.options = options
185 self.modified_after = options.begin
186 self.modified_before = options.end
187 self.user = options.user
188 self.changes = []
189 self.reviews = []
190 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100191 self.referenced_issues = []
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000192 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-14 19:02:55 -0700193 self.access_errors = set()
Vadim Bendebury80012972019-11-23 02:23:11 +0000194 self.skip_servers = (options.skip_servers.split(','))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000195
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100196 def show_progress(self, how='.'):
197 if sys.stdout.isatty():
198 sys.stdout.write(how)
199 sys.stdout.flush()
200
Vadim Bendebury8de38002018-05-14 19:02:55 -0700201 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200202 # Convert the "key:value" filter to a list of (key, value) pairs.
203 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000204 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000205 # Instantiate the generator to force all the requests now and catch the
206 # errors here.
207 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000208 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
209 'CURRENT_REVISION', 'CURRENT_COMMIT']))
Raul Tambre7c938462019-05-24 16:35:35 +0000210 except gerrit_util.GerritError as e:
Vadim Bendebury8de38002018-05-14 19:02:55 -0700211 error_message = 'Looking up %r: %s' % (instance['url'], e)
212 if error_message not in self.access_errors:
213 self.access_errors.add(error_message)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000214 return []
215
deymo@chromium.org6c039202013-09-12 12:28:12 +0000216 def gerrit_search(self, instance, owner=None, reviewer=None):
Vadim Bendebury80012972019-11-23 02:23:11 +0000217 if instance['url'] in self.skip_servers:
218 return []
deymo@chromium.org6c039202013-09-12 12:28:12 +0000219 max_age = datetime.today() - self.modified_after
Andrii Shyshkalov4aedec02018-09-17 16:31:17 +0000220 filters = ['-age:%ss' % (max_age.days * 24 * 3600 + max_age.seconds)]
221 if owner:
222 assert not reviewer
223 filters.append('owner:%s' % owner)
224 else:
225 filters.extend(('-owner:%s' % reviewer, 'reviewer:%s' % reviewer))
Peter K. Lee9342ac02018-08-28 21:03:51 +0000226 # TODO(cjhopman): Should abandoned changes be filtered out when
227 # merged_only is not enabled?
228 if self.options.merged_only:
229 filters.append('status:merged')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000230
Aaron Gable2979a872017-09-05 17:38:32 -0700231 issues = self.gerrit_changes_over_rest(instance, filters)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100232 self.show_progress()
Aaron Gable2979a872017-09-05 17:38:32 -0700233 issues = [self.process_gerrit_issue(instance, issue)
234 for issue in issues]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000235
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000236 issues = filter(self.filter_issue, issues)
237 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
238
239 return issues
240
Aaron Gable2979a872017-09-05 17:38:32 -0700241 def process_gerrit_issue(self, instance, issue):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000242 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000243 if self.options.deltas:
244 ret['delta'] = DefaultFormatter().format(
245 '+{insertions},-{deletions}',
246 **issue)
247 ret['status'] = issue['status']
deymo@chromium.org6c039202013-09-12 12:28:12 +0000248 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700249 protocol = instance.get('short_url_protocol', 'http')
250 url = instance['shorturl']
251 else:
252 protocol = 'https'
253 url = instance['url']
254 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
255
deymo@chromium.org6c039202013-09-12 12:28:12 +0000256 ret['header'] = issue['subject']
Don Garrett2ebf9fd2018-08-06 15:14:00 +0000257 ret['owner'] = issue['owner'].get('email', '')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000258 ret['author'] = ret['owner']
259 ret['created'] = datetime_from_gerrit(issue['created'])
260 ret['modified'] = datetime_from_gerrit(issue['updated'])
261 if 'messages' in issue:
Aaron Gable2979a872017-09-05 17:38:32 -0700262 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
deymo@chromium.org6c039202013-09-12 12:28:12 +0000263 else:
264 ret['replies'] = []
265 ret['reviewers'] = set(r['author'] for r in ret['replies'])
266 ret['reviewers'].discard(ret['author'])
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000267 ret['bugs'] = extract_bug_numbers_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000268 return ret
269
270 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700271 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000272 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000273 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
274 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000275 for reply in replies:
276 ret.append({
277 'author': reply['author']['email'],
278 'created': datetime_from_gerrit(reply['date']),
279 'content': reply['message'],
280 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000281 return ret
282
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100283 def monorail_get_auth_http(self):
Kenneth Russell66badbd2018-09-09 21:35:32 +0000284 # Manually use a long timeout (10m); for some users who have a
285 # long history on the issue tracker, whatever the default timeout
286 # is is reached.
Edward Lemur5b929a42019-10-21 17:57:39 +0000287 return auth.Authenticator().authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100288
289 def filter_modified_monorail_issue(self, issue):
290 """Precisely checks if an issue has been modified in the time range.
291
292 This fetches all issue comments to check if the issue has been modified in
293 the time range specified by user. This is needed because monorail only
294 allows filtering by last updated and published dates, which is not
295 sufficient to tell whether a given issue has been modified at some specific
296 time range. Any update to the issue is a reported as comment on Monorail.
297
298 Args:
299 issue: Issue dict as returned by monorail_query_issues method. In
300 particular, must have a key 'uid' formatted as 'project:issue_id'.
301
302 Returns:
303 Passed issue if modified, None otherwise.
304 """
305 http = self.monorail_get_auth_http()
306 project, issue_id = issue['uid'].split(':')
307 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
308 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
309 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100310 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100311 content = json.loads(body)
312 if not content:
313 logging.error('Unable to parse %s response from monorail.', project)
314 return issue
315
316 for item in content.get('items', []):
317 comment_published = datetime_from_monorail(item['published'])
318 if self.filter_modified(comment_published):
319 return issue
320
321 return None
322
323 def monorail_query_issues(self, project, query):
324 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000325 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100326 '/%s/issues') % project
Edward Lemur2a048032020-01-14 22:58:13 +0000327 query_data = urllib_parse.urlencode(query)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100328 url = url + '?' + query_data
329 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100330 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100331 content = json.loads(body)
332 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100333 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100334 return []
335
336 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100337 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100338 for item in content.get('items', []):
339 if project_config.get('shorturl'):
340 protocol = project_config.get('short_url_protocol', 'http')
341 item_url = '%s://%s/%d' % (
342 protocol, project_config['shorturl'], item['id'])
343 else:
344 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
345 project, item['id'])
346 issue = {
347 'uid': '%s:%s' % (project, item['id']),
348 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100349 'created': datetime_from_monorail(item['published']),
350 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100351 'author': item['author']['name'],
352 'url': item_url,
353 'comments': [],
354 'status': item['status'],
355 'labels': [],
356 'components': []
357 }
358 if 'owner' in item:
359 issue['owner'] = item['owner']['name']
360 else:
361 issue['owner'] = 'None'
362 if 'labels' in item:
363 issue['labels'] = item['labels']
364 if 'components' in item:
365 issue['components'] = item['components']
366 issues.append(issue)
367
368 return issues
369
370 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000371 epoch = datetime.utcfromtimestamp(0)
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000372 # Defaults to @chromium.org email if one wasn't provided on -u option.
373 user_str = (self.options.email if self.options.email.find('@') >= 0
374 else '%s@chromium.org' % self.user)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000375
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100376 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000377 'maxResults': 10000,
378 'q': user_str,
379 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
380 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000381 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000382
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000383 if self.options.completed_issues:
384 return [
385 issue for issue in issues
386 if (self.match(issue['owner']) and
387 issue['status'].lower() in ('verified', 'fixed'))
388 ]
389
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100390 return [
391 issue for issue in issues
392 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000393
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100394 def monorail_get_issues(self, project, issue_ids):
395 return self.monorail_query_issues(project, {
396 'maxResults': 10000,
397 'q': 'id:%s' % ','.join(issue_ids)
398 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000399
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000400 def print_heading(self, heading):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000401 print()
402 print(self.options.output_format_heading.format(heading=heading))
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000403
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000404 def match(self, author):
405 if '@' in self.user:
406 return author == self.user
407 return author.startswith(self.user + '@')
408
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000409 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000410 activity = len([
411 reply
412 for reply in change['replies']
413 if self.match(reply['author'])
414 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000415 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000416 'created': change['created'].date().isoformat(),
417 'modified': change['modified'].date().isoformat(),
418 'reviewers': ', '.join(change['reviewers']),
419 'status': change['status'],
420 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000421 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000422 if self.options.deltas:
423 optional_values['delta'] = change['delta']
424
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000425 self.print_generic(self.options.output_format,
426 self.options.output_format_changes,
427 change['header'],
428 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000429 change['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000430 change['created'],
431 change['modified'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000432 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000433
434 def print_issue(self, issue):
435 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000436 'created': issue['created'].date().isoformat(),
437 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000438 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000439 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000440 }
441 self.print_generic(self.options.output_format,
442 self.options.output_format_issues,
443 issue['header'],
444 issue['url'],
445 issue['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000446 issue['created'],
447 issue['modified'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000448 optional_values)
449
450 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000451 activity = len([
452 reply
453 for reply in review['replies']
454 if self.match(reply['author'])
455 ])
456 optional_values = {
457 'created': review['created'].date().isoformat(),
458 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800459 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000460 'activity': activity,
461 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800462 if self.options.deltas:
463 optional_values['delta'] = review['delta']
464
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000465 self.print_generic(self.options.output_format,
466 self.options.output_format_reviews,
467 review['header'],
468 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000469 review['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000470 review['created'],
471 review['modified'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000472 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000473
474 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000475 def print_generic(default_fmt, specific_fmt,
Lutz Justen860378e2019-03-12 01:10:02 +0000476 title, url, author, created, modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000477 optional_values=None):
478 output_format = specific_fmt if specific_fmt is not None else default_fmt
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000479 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000480 'title': title,
481 'url': url,
482 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000483 'created': created,
484 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000485 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000486 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000487 values.update(optional_values)
Edward Lemur2a048032020-01-14 22:58:13 +0000488 print(DefaultFormatter().format(output_format, **values))
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000489
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000490
491 def filter_issue(self, issue, should_filter_by_user=True):
492 def maybe_filter_username(email):
493 return not should_filter_by_user or username(email) == self.user
494 if (maybe_filter_username(issue['author']) and
495 self.filter_modified(issue['created'])):
496 return True
497 if (maybe_filter_username(issue['owner']) and
498 (self.filter_modified(issue['created']) or
499 self.filter_modified(issue['modified']))):
500 return True
501 for reply in issue['replies']:
502 if self.filter_modified(reply['created']):
503 if not should_filter_by_user:
504 break
505 if (username(reply['author']) == self.user
506 or (self.user + '@') in reply['content']):
507 break
508 else:
509 return False
510 return True
511
512 def filter_modified(self, modified):
513 return self.modified_after < modified and modified < self.modified_before
514
515 def auth_for_changes(self):
516 #TODO(cjhopman): Move authentication check for getting changes here.
517 pass
518
519 def auth_for_reviews(self):
520 # Reviews use all the same instances as changes so no authentication is
521 # required.
522 pass
523
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000524 def get_changes(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000525 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100526 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100527 gerrit_changes = pool.map_async(
528 lambda instance: self.gerrit_search(instance, owner=self.user),
529 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100530 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000531 self.changes = list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000532
533 def print_changes(self):
534 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000535 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000536 for change in self.changes:
Bruce Dawson2afcf222019-02-25 22:07:08 +0000537 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000538
Vadim Bendebury8de38002018-05-14 19:02:55 -0700539 def print_access_errors(self):
540 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400541 logging.error('Access Errors:')
542 for error in self.access_errors:
543 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700544
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000545 def get_reviews(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000546 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100547 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100548 gerrit_reviews = pool.map_async(
549 lambda instance: self.gerrit_search(instance, reviewer=self.user),
550 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100551 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000552 self.reviews = list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000553
554 def print_reviews(self):
555 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000556 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000557 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000558 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000559
560 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100561 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
562 monorail_issues = pool.map(
563 self.monorail_issue_search, monorail_projects.keys())
564 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
565
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700566 if not monorail_issues:
567 return
568
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100569 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
570 filtered_issues = pool.map(
571 self.filter_modified_monorail_issue, monorail_issues)
572 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100573
574 def get_referenced_issues(self):
575 if not self.issues:
576 self.get_issues()
577
578 if not self.changes:
579 self.get_changes()
580
581 referenced_issue_uids = set(itertools.chain.from_iterable(
582 change['bugs'] for change in self.changes))
583 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
584 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
585
586 missing_issues_by_project = collections.defaultdict(list)
587 for issue_uid in missing_issue_uids:
588 project, issue_id = issue_uid.split(':')
589 missing_issues_by_project[project].append(issue_id)
590
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000591 for project, issue_ids in missing_issues_by_project.items():
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100592 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000593
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000594 def print_issues(self):
595 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000596 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000597 for issue in self.issues:
598 self.print_issue(issue)
599
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100600 def print_changes_by_issue(self, skip_empty_own):
601 if not self.issues or not self.changes:
602 return
603
604 self.print_heading('Changes by referenced issue(s)')
605 issues = {issue['uid']: issue for issue in self.issues}
606 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
607 changes_by_issue_uid = collections.defaultdict(list)
608 changes_by_ref_issue_uid = collections.defaultdict(list)
609 changes_without_issue = []
610 for change in self.changes:
611 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000612 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100613 if issue_uid in issues:
614 changes_by_issue_uid[issue_uid].append(change)
615 added = True
616 if issue_uid in ref_issues:
617 changes_by_ref_issue_uid[issue_uid].append(change)
618 added = True
619 if not added:
620 changes_without_issue.append(change)
621
622 # Changes referencing own issues.
623 for issue_uid in issues:
624 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
625 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000626 if changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000627 print()
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000628 for change in changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000629 print(' ', end='') # this prints no newline
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000630 self.print_change(change)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000631 print()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100632
633 # Changes referencing others' issues.
634 for issue_uid in ref_issues:
635 assert changes_by_ref_issue_uid[issue_uid]
636 self.print_issue(ref_issues[issue_uid])
637 for change in changes_by_ref_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000638 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100639 self.print_change(change)
640
641 # Changes referencing no issues.
642 if changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000643 print(self.options.output_format_no_url.format(title='Other changes'))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100644 for change in changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000645 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100646 self.print_change(change)
647
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000648 def print_activity(self):
649 self.print_changes()
650 self.print_reviews()
651 self.print_issues()
652
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000653 def dump_json(self, ignore_keys=None):
654 if ignore_keys is None:
655 ignore_keys = ['replies']
656
657 def format_for_json_dump(in_array):
658 output = {}
659 for item in in_array:
660 url = item.get('url') or item.get('review_url')
661 if not url:
662 raise Exception('Dumped item %s does not specify url' % item)
663 output[url] = dict(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000664 (k, v) for k,v in item.items() if k not in ignore_keys)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000665 return output
666
667 class PythonObjectEncoder(json.JSONEncoder):
668 def default(self, obj): # pylint: disable=method-hidden
669 if isinstance(obj, datetime):
670 return obj.isoformat()
671 if isinstance(obj, set):
672 return list(obj)
673 return json.JSONEncoder.default(self, obj)
674
675 output = {
676 'reviews': format_for_json_dump(self.reviews),
677 'changes': format_for_json_dump(self.changes),
678 'issues': format_for_json_dump(self.issues)
679 }
Raul Tambre80ee78e2019-05-06 22:41:05 +0000680 print(json.dumps(output, indent=2, cls=PythonObjectEncoder))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000681
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000682
683def main():
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000684 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
685 parser.add_option(
686 '-u', '--user', metavar='<email>',
Bruce Dawson84370962019-02-21 05:33:37 +0000687 # Look for USER and USERNAME (Windows) environment variables.
688 default=os.environ.get('USER', os.environ.get('USERNAME')),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000689 help='Filter on user, default=%default')
690 parser.add_option(
691 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000692 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000693 parser.add_option(
694 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000695 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000696 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
697 relativedelta(months=2))
698 parser.add_option(
699 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000700 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000701 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
702 parser.add_option(
703 '-Y', '--this_year', action='store_true',
704 help='Use this year\'s dates')
705 parser.add_option(
706 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000707 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000708 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000709 '-W', '--last_week', action='count',
710 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000711 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000712 '-a', '--auth',
713 action='store_true',
714 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000715 parser.add_option(
716 '-d', '--deltas',
717 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800718 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100719 parser.add_option(
720 '--no-referenced-issues',
721 action='store_true',
722 help='Do not fetch issues referenced by owned changes. Useful in '
723 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100724 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100725 parser.add_option(
Vadim Bendebury80012972019-11-23 02:23:11 +0000726 '--skip_servers',
727 action='store',
728 default='',
729 help='A comma separated list of gerrit and rietveld servers to ignore')
730 parser.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100731 '--skip-own-issues-without-changes',
732 action='store_true',
733 help='Skips listing own issues without changes when showing changes '
734 'grouped by referenced issue(s). See --changes-by-issue for more '
735 'details.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000736
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000737 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000738 'By default, all activity will be looked up and '
739 'printed. If any of these are specified, only '
740 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000741 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000742 '-c', '--changes',
743 action='store_true',
744 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000745 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000746 '-i', '--issues',
747 action='store_true',
748 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000749 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000750 '-r', '--reviews',
751 action='store_true',
752 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100753 activity_types_group.add_option(
754 '--changes-by-issue', action='store_true',
755 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000756 parser.add_option_group(activity_types_group)
757
758 output_format_group = optparse.OptionGroup(parser, 'Output Format',
759 'By default, all activity will be printed in the '
760 'following format: {url} {title}. This can be '
761 'changed for either all activity types or '
762 'individually for each activity type. The format '
763 'is defined as documented for '
764 'string.format(...). The variables available for '
Lutz Justen860378e2019-03-12 01:10:02 +0000765 'all activity types are url, title, author, '
766 'created and modified. Format options for '
767 'specific activity types will override the '
768 'generic format.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000769 output_format_group.add_option(
770 '-f', '--output-format', metavar='<format>',
771 default=u'{url} {title}',
772 help='Specifies the format to use when printing all your activity.')
773 output_format_group.add_option(
774 '--output-format-changes', metavar='<format>',
775 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000776 help='Specifies the format to use when printing changes. Supports the '
777 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000778 output_format_group.add_option(
779 '--output-format-issues', metavar='<format>',
780 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000781 help='Specifies the format to use when printing issues. Supports the '
782 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000783 output_format_group.add_option(
784 '--output-format-reviews', metavar='<format>',
785 default=None,
786 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000787 output_format_group.add_option(
788 '--output-format-heading', metavar='<format>',
789 default=u'{heading}:',
790 help='Specifies the format to use when printing headings.')
791 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100792 '--output-format-no-url', default='{title}',
793 help='Specifies the format to use when printing activity without url.')
794 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000795 '-m', '--markdown', action='store_true',
796 help='Use markdown-friendly output (overrides --output-format '
797 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000798 output_format_group.add_option(
799 '-j', '--json', action='store_true',
800 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000801 parser.add_option_group(output_format_group)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000802
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000803 parser.add_option(
804 '-v', '--verbose',
805 action='store_const',
806 dest='verbosity',
807 default=logging.WARN,
808 const=logging.INFO,
809 help='Output extra informational messages.'
810 )
811 parser.add_option(
812 '-q', '--quiet',
813 action='store_const',
814 dest='verbosity',
815 const=logging.ERROR,
816 help='Suppress non-error messages.'
817 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000818 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000819 '-M', '--merged-only',
820 action='store_true',
821 dest='merged_only',
822 default=False,
823 help='Shows only changes that have been merged.')
824 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000825 '-C', '--completed-issues',
826 action='store_true',
827 dest='completed_issues',
828 default=False,
829 help='Shows only monorail issues that have completed (Fixed|Verified) '
830 'by the user.')
831 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000832 '-o', '--output', metavar='<file>',
833 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000834
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000835 # Remove description formatting
836 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800837 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000838
839 options, args = parser.parse_args()
840 options.local_user = os.environ.get('USER')
841 if args:
842 parser.error('Args unsupported')
843 if not options.user:
Bruce Dawson84370962019-02-21 05:33:37 +0000844 parser.error('USER/USERNAME is not set, please use -u')
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000845 # Retains the original -u option as the email address.
846 options.email = options.user
847 options.user = username(options.email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000848
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000849 logging.basicConfig(level=options.verbosity)
850
851 # python-keyring provides easy access to the system keyring.
852 try:
853 import keyring # pylint: disable=unused-import,unused-variable,F0401
854 except ImportError:
855 logging.warning('Consider installing python-keyring')
856
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000857 if not options.begin:
858 if options.last_quarter:
859 begin, end = quarter_begin, quarter_end
860 elif options.this_year:
861 begin, end = get_year_of(datetime.today())
862 elif options.week_of:
863 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000864 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000865 begin, end = (get_week_of(datetime.today() -
866 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000867 else:
868 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
869 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700870 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000871 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700872 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000873 else:
874 end = datetime.today()
875 options.begin, options.end = begin, end
Bruce Dawson2afcf222019-02-25 22:07:08 +0000876 if begin >= end:
877 # The queries fail in peculiar ways when the begin date is in the future.
878 # Give a descriptive error message instead.
879 logging.error('Start date (%s) is the same or later than end date (%s)' %
880 (begin, end))
881 return 1
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000882
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000883 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000884 options.output_format_heading = '### {heading}\n'
885 options.output_format = ' * [{title}]({url})'
886 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000887 logging.info('Searching for activity by %s', options.user)
888 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000889
890 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100891 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000892
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100893 if not (options.changes or options.reviews or options.issues or
894 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000895 options.changes = True
896 options.issues = True
897 options.reviews = True
898
899 # First do any required authentication so none of the user interaction has to
900 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100901 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000902 my_activity.auth_for_changes()
903 if options.reviews:
904 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000905
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000906 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000907
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000908 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100909 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000910 my_activity.get_changes()
911 if options.reviews:
912 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100913 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000914 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100915 if not options.no_referenced_issues:
916 my_activity.get_referenced_issues()
Edward Lemur5b929a42019-10-21 17:57:39 +0000917 except auth.LoginRequiredError as e:
918 logging.error('auth.LoginRequiredError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000919
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100920 my_activity.show_progress('\n')
921
Vadim Bendebury8de38002018-05-14 19:02:55 -0700922 my_activity.print_access_errors()
923
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000924 output_file = None
925 try:
926 if options.output:
927 output_file = open(options.output, 'w')
928 logging.info('Printing output to "%s"', options.output)
929 sys.stdout = output_file
930 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700931 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000932 else:
933 if options.json:
934 my_activity.dump_json()
935 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100936 if options.changes:
937 my_activity.print_changes()
938 if options.reviews:
939 my_activity.print_reviews()
940 if options.issues:
941 my_activity.print_issues()
942 if options.changes_by_issue:
943 my_activity.print_changes_by_issue(
944 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000945 finally:
946 if output_file:
947 logging.info('Done printing to file.')
948 sys.stdout = sys.__stdout__
949 output_file.close()
950
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000951 return 0
952
953
954if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000955 # Fix encoding to support non-ascii issue titles.
956 fix_encoding.fix_encoding()
957
sbc@chromium.org013731e2015-02-26 18:28:43 +0000958 try:
959 sys.exit(main())
960 except KeyboardInterrupt:
961 sys.stderr.write('interrupted\n')
962 sys.exit(1)