blob: 774b60c7da552079077af47215dcb0986ec4d81c [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
Gabriel Charettebc6617a2019-02-05 21:30:52 +000024# [VPYTHON:BEGIN]
25# wheel: <
26# name: "infra/python/wheels/python-dateutil-py2_py3"
27# version: "version:2.7.3"
28# >
29# wheel: <
30# name: "infra/python/wheels/six-py2_py3"
31# version: "version:1.10.0"
32# >
33# [VPYTHON:END]
34
Raul Tambre80ee78e2019-05-06 22:41:05 +000035from __future__ import print_function
36
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010037import collections
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +010038import contextlib
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000039from datetime import datetime
40from datetime import timedelta
41from functools import partial
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010042import itertools
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000043import json
Tobias Sargeantffb3c432017-03-08 14:09:14 +000044import logging
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +010045from multiprocessing.pool import ThreadPool
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000046import optparse
47import os
48import subprocess
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
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000056import gerrit_util
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000057
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +000058from third_party import httplib2
59
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000060try:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000061 import dateutil # pylint: disable=import-error
62 import dateutil.parser
63 from dateutil.relativedelta import relativedelta
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000064except ImportError:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000065 logging.error('python-dateutil package required')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000066 exit(1)
67
Tobias Sargeantffb3c432017-03-08 14:09:14 +000068
69class DefaultFormatter(Formatter):
70 def __init__(self, default = ''):
71 super(DefaultFormatter, self).__init__()
72 self.default = default
73
74 def get_value(self, key, args, kwds):
75 if isinstance(key, basestring) and key not in kwds:
76 return self.default
77 return Formatter.get_value(self, key, args, kwds)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000078
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000079gerrit_instances = [
80 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000081 'url': 'android-review.googlesource.com',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000082 },
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000083 {
84 'url': 'chrome-internal-review.googlesource.com',
Jeremy Romanf475a472017-06-06 16:49:11 -040085 'shorturl': 'crrev.com/i',
Varun Khanejad9f97bc2017-08-02 12:55:01 -070086 'short_url_protocol': 'https',
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000087 },
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000088 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000089 'url': 'chromium-review.googlesource.com',
90 'shorturl': 'crrev.com/c',
91 'short_url_protocol': 'https',
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000092 },
Ryan Harrison897602a2017-09-18 16:23:41 -040093 {
94 'url': 'pdfium-review.googlesource.com',
95 },
Adrienne Walker95d4c852018-09-27 20:28:12 +000096 {
97 'url': 'skia-review.googlesource.com',
98 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000099]
100
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100101monorail_projects = {
Jamie Madill1db68ea2019-09-03 19:34:42 +0000102 'angleproject': {
103 'shorturl': 'anglebug.com',
104 'short_url_protocol': 'http',
105 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100106 'chromium': {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000107 'shorturl': 'crbug.com',
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700108 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000109 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100110 'google-breakpad': {},
111 'gyp': {},
112 'skia': {},
113 'pdfium': {
Ryan Harrison897602a2017-09-18 16:23:41 -0400114 'shorturl': 'crbug.com/pdfium',
115 'short_url_protocol': 'https',
116 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100117 'v8': {
118 'shorturl': 'crbug.com/v8',
119 'short_url_protocol': 'https',
120 },
121}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000122
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000123def username(email):
124 """Keeps the username of an email address."""
125 return email and email.split('@', 1)[0]
126
127
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000128def datetime_to_midnight(date):
129 return date - timedelta(hours=date.hour, minutes=date.minute,
130 seconds=date.second, microseconds=date.microsecond)
131
132
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000133def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000134 begin = (datetime_to_midnight(date) -
135 relativedelta(months=(date.month % 3) - 1, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000136 return begin, begin + relativedelta(months=3)
137
138
139def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000140 begin = (datetime_to_midnight(date) -
141 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000142 return begin, begin + relativedelta(years=1)
143
144
145def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000146 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000147 return begin, begin + timedelta(days=7)
148
149
150def get_yes_or_no(msg):
151 while True:
152 response = raw_input(msg + ' yes/no [no] ')
153 if response == 'y' or response == 'yes':
154 return True
155 elif not response or response == 'n' or response == 'no':
156 return False
157
158
deymo@chromium.org6c039202013-09-12 12:28:12 +0000159def datetime_from_gerrit(date_string):
160 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
161
162
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100163def datetime_from_monorail(date_string):
164 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000165
166
167class MyActivity(object):
168 def __init__(self, options):
169 self.options = options
170 self.modified_after = options.begin
171 self.modified_before = options.end
172 self.user = options.user
173 self.changes = []
174 self.reviews = []
175 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100176 self.referenced_issues = []
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000177 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-14 19:02:55 -0700178 self.access_errors = set()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000179
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100180 def show_progress(self, how='.'):
181 if sys.stdout.isatty():
182 sys.stdout.write(how)
183 sys.stdout.flush()
184
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000185 def extract_bug_numbers_from_description(self, issue):
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000186 description = None
187
188 if 'description' in issue:
189 # Getting the description for Rietveld
190 description = issue['description']
191 elif 'revisions' in issue:
192 # Getting the description for REST Gerrit
193 revision = issue['revisions'][issue['current_revision']]
194 description = revision['commit']['message']
195
196 bugs = []
197 if description:
Nicolas Dossou-gbete903ea732017-07-10 16:46:59 +0100198 # Handle both "Bug: 99999" and "BUG=99999" bug notations
199 # Multiple bugs can be noted on a single line or in multiple ones.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100200 matches = re.findall(
201 r'BUG[=:]\s?((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)', description,
202 flags=re.IGNORECASE)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000203 if matches:
204 for match in matches:
205 bugs.extend(match[0].replace(' ', '').split(','))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100206 # Add default chromium: prefix if none specified.
207 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000208
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000209 return sorted(set(bugs))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000210
Vadim Bendebury8de38002018-05-14 19:02:55 -0700211 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200212 # Convert the "key:value" filter to a list of (key, value) pairs.
213 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000214 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000215 # Instantiate the generator to force all the requests now and catch the
216 # errors here.
217 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000218 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
219 'CURRENT_REVISION', 'CURRENT_COMMIT']))
Raul Tambre7c938462019-05-24 16:35:35 +0000220 except gerrit_util.GerritError as e:
Vadim Bendebury8de38002018-05-14 19:02:55 -0700221 error_message = 'Looking up %r: %s' % (instance['url'], e)
222 if error_message not in self.access_errors:
223 self.access_errors.add(error_message)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000224 return []
225
deymo@chromium.org6c039202013-09-12 12:28:12 +0000226 def gerrit_search(self, instance, owner=None, reviewer=None):
227 max_age = datetime.today() - self.modified_after
Andrii Shyshkalov4aedec02018-09-17 16:31:17 +0000228 filters = ['-age:%ss' % (max_age.days * 24 * 3600 + max_age.seconds)]
229 if owner:
230 assert not reviewer
231 filters.append('owner:%s' % owner)
232 else:
233 filters.extend(('-owner:%s' % reviewer, 'reviewer:%s' % reviewer))
Peter K. Lee9342ac02018-08-28 21:03:51 +0000234 # TODO(cjhopman): Should abandoned changes be filtered out when
235 # merged_only is not enabled?
236 if self.options.merged_only:
237 filters.append('status:merged')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000238
Aaron Gable2979a872017-09-05 17:38:32 -0700239 issues = self.gerrit_changes_over_rest(instance, filters)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100240 self.show_progress()
Aaron Gable2979a872017-09-05 17:38:32 -0700241 issues = [self.process_gerrit_issue(instance, issue)
242 for issue in issues]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000243
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000244 issues = filter(self.filter_issue, issues)
245 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
246
247 return issues
248
Aaron Gable2979a872017-09-05 17:38:32 -0700249 def process_gerrit_issue(self, instance, issue):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000250 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000251 if self.options.deltas:
252 ret['delta'] = DefaultFormatter().format(
253 '+{insertions},-{deletions}',
254 **issue)
255 ret['status'] = issue['status']
deymo@chromium.org6c039202013-09-12 12:28:12 +0000256 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700257 protocol = instance.get('short_url_protocol', 'http')
258 url = instance['shorturl']
259 else:
260 protocol = 'https'
261 url = instance['url']
262 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
263
deymo@chromium.org6c039202013-09-12 12:28:12 +0000264 ret['header'] = issue['subject']
Don Garrett2ebf9fd2018-08-06 15:14:00 +0000265 ret['owner'] = issue['owner'].get('email', '')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000266 ret['author'] = ret['owner']
267 ret['created'] = datetime_from_gerrit(issue['created'])
268 ret['modified'] = datetime_from_gerrit(issue['updated'])
269 if 'messages' in issue:
Aaron Gable2979a872017-09-05 17:38:32 -0700270 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
deymo@chromium.org6c039202013-09-12 12:28:12 +0000271 else:
272 ret['replies'] = []
273 ret['reviewers'] = set(r['author'] for r in ret['replies'])
274 ret['reviewers'].discard(ret['author'])
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000275 ret['bugs'] = self.extract_bug_numbers_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000276 return ret
277
278 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700279 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000280 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000281 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
282 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000283 for reply in replies:
284 ret.append({
285 'author': reply['author']['email'],
286 'created': datetime_from_gerrit(reply['date']),
287 'content': reply['message'],
288 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000289 return ret
290
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100291 def monorail_get_auth_http(self):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000292 auth_config = auth.extract_auth_config_from_options(self.options)
293 authenticator = auth.get_authenticator_for_host(
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000294 'bugs.chromium.org', auth_config)
Kenneth Russell66badbd2018-09-09 21:35:32 +0000295 # Manually use a long timeout (10m); for some users who have a
296 # long history on the issue tracker, whatever the default timeout
297 # is is reached.
298 return authenticator.authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100299
300 def filter_modified_monorail_issue(self, issue):
301 """Precisely checks if an issue has been modified in the time range.
302
303 This fetches all issue comments to check if the issue has been modified in
304 the time range specified by user. This is needed because monorail only
305 allows filtering by last updated and published dates, which is not
306 sufficient to tell whether a given issue has been modified at some specific
307 time range. Any update to the issue is a reported as comment on Monorail.
308
309 Args:
310 issue: Issue dict as returned by monorail_query_issues method. In
311 particular, must have a key 'uid' formatted as 'project:issue_id'.
312
313 Returns:
314 Passed issue if modified, None otherwise.
315 """
316 http = self.monorail_get_auth_http()
317 project, issue_id = issue['uid'].split(':')
318 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
319 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
320 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100321 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100322 content = json.loads(body)
323 if not content:
324 logging.error('Unable to parse %s response from monorail.', project)
325 return issue
326
327 for item in content.get('items', []):
328 comment_published = datetime_from_monorail(item['published'])
329 if self.filter_modified(comment_published):
330 return issue
331
332 return None
333
334 def monorail_query_issues(self, project, query):
335 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000336 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100337 '/%s/issues') % project
338 query_data = urllib.urlencode(query)
339 url = url + '?' + query_data
340 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100341 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100342 content = json.loads(body)
343 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100344 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100345 return []
346
347 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100348 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100349 for item in content.get('items', []):
350 if project_config.get('shorturl'):
351 protocol = project_config.get('short_url_protocol', 'http')
352 item_url = '%s://%s/%d' % (
353 protocol, project_config['shorturl'], item['id'])
354 else:
355 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
356 project, item['id'])
357 issue = {
358 'uid': '%s:%s' % (project, item['id']),
359 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100360 'created': datetime_from_monorail(item['published']),
361 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100362 'author': item['author']['name'],
363 'url': item_url,
364 'comments': [],
365 'status': item['status'],
366 'labels': [],
367 'components': []
368 }
369 if 'owner' in item:
370 issue['owner'] = item['owner']['name']
371 else:
372 issue['owner'] = 'None'
373 if 'labels' in item:
374 issue['labels'] = item['labels']
375 if 'components' in item:
376 issue['components'] = item['components']
377 issues.append(issue)
378
379 return issues
380
381 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000382 epoch = datetime.utcfromtimestamp(0)
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000383 # TODO(tandrii): support non-chromium email, too.
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000384 user_str = '%s@chromium.org' % self.user
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000385
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100386 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000387 'maxResults': 10000,
388 'q': user_str,
389 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
390 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000391 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000392
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000393 if self.options.completed_issues:
394 return [
395 issue for issue in issues
396 if (self.match(issue['owner']) and
397 issue['status'].lower() in ('verified', 'fixed'))
398 ]
399
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100400 return [
401 issue for issue in issues
402 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000403
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100404 def monorail_get_issues(self, project, issue_ids):
405 return self.monorail_query_issues(project, {
406 'maxResults': 10000,
407 'q': 'id:%s' % ','.join(issue_ids)
408 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000409
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000410 def print_heading(self, heading):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000411 print()
412 print(self.options.output_format_heading.format(heading=heading))
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000413
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000414 def match(self, author):
415 if '@' in self.user:
416 return author == self.user
417 return author.startswith(self.user + '@')
418
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000419 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000420 activity = len([
421 reply
422 for reply in change['replies']
423 if self.match(reply['author'])
424 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000425 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000426 'created': change['created'].date().isoformat(),
427 'modified': change['modified'].date().isoformat(),
428 'reviewers': ', '.join(change['reviewers']),
429 'status': change['status'],
430 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000431 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000432 if self.options.deltas:
433 optional_values['delta'] = change['delta']
434
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000435 self.print_generic(self.options.output_format,
436 self.options.output_format_changes,
437 change['header'],
438 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000439 change['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000440 change['created'],
441 change['modified'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000442 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000443
444 def print_issue(self, issue):
445 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000446 'created': issue['created'].date().isoformat(),
447 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000448 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000449 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000450 }
451 self.print_generic(self.options.output_format,
452 self.options.output_format_issues,
453 issue['header'],
454 issue['url'],
455 issue['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000456 issue['created'],
457 issue['modified'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000458 optional_values)
459
460 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000461 activity = len([
462 reply
463 for reply in review['replies']
464 if self.match(reply['author'])
465 ])
466 optional_values = {
467 'created': review['created'].date().isoformat(),
468 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800469 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000470 'activity': activity,
471 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800472 if self.options.deltas:
473 optional_values['delta'] = review['delta']
474
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000475 self.print_generic(self.options.output_format,
476 self.options.output_format_reviews,
477 review['header'],
478 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000479 review['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000480 review['created'],
481 review['modified'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000482 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000483
484 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000485 def print_generic(default_fmt, specific_fmt,
Lutz Justen860378e2019-03-12 01:10:02 +0000486 title, url, author, created, modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000487 optional_values=None):
488 output_format = specific_fmt if specific_fmt is not None else default_fmt
489 output_format = unicode(output_format)
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000490 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000491 'title': title,
492 'url': url,
493 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000494 'created': created,
495 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000496 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000497 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000498 values.update(optional_values)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000499 print(DefaultFormatter().format(output_format,
500 **values).encode(sys.getdefaultencoding()))
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000501
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000502
503 def filter_issue(self, issue, should_filter_by_user=True):
504 def maybe_filter_username(email):
505 return not should_filter_by_user or username(email) == self.user
506 if (maybe_filter_username(issue['author']) and
507 self.filter_modified(issue['created'])):
508 return True
509 if (maybe_filter_username(issue['owner']) and
510 (self.filter_modified(issue['created']) or
511 self.filter_modified(issue['modified']))):
512 return True
513 for reply in issue['replies']:
514 if self.filter_modified(reply['created']):
515 if not should_filter_by_user:
516 break
517 if (username(reply['author']) == self.user
518 or (self.user + '@') in reply['content']):
519 break
520 else:
521 return False
522 return True
523
524 def filter_modified(self, modified):
525 return self.modified_after < modified and modified < self.modified_before
526
527 def auth_for_changes(self):
528 #TODO(cjhopman): Move authentication check for getting changes here.
529 pass
530
531 def auth_for_reviews(self):
532 # Reviews use all the same instances as changes so no authentication is
533 # required.
534 pass
535
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000536 def get_changes(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000537 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100538 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100539 gerrit_changes = pool.map_async(
540 lambda instance: self.gerrit_search(instance, owner=self.user),
541 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100542 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000543 self.changes = list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000544
545 def print_changes(self):
546 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000547 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000548 for change in self.changes:
Bruce Dawson2afcf222019-02-25 22:07:08 +0000549 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000550
Vadim Bendebury8de38002018-05-14 19:02:55 -0700551 def print_access_errors(self):
552 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400553 logging.error('Access Errors:')
554 for error in self.access_errors:
555 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700556
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000557 def get_reviews(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000558 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100559 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100560 gerrit_reviews = pool.map_async(
561 lambda instance: self.gerrit_search(instance, reviewer=self.user),
562 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100563 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000564 self.reviews = list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000565
566 def print_reviews(self):
567 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000568 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000569 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000570 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000571
572 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100573 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
574 monorail_issues = pool.map(
575 self.monorail_issue_search, monorail_projects.keys())
576 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
577
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700578 if not monorail_issues:
579 return
580
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100581 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
582 filtered_issues = pool.map(
583 self.filter_modified_monorail_issue, monorail_issues)
584 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100585
586 def get_referenced_issues(self):
587 if not self.issues:
588 self.get_issues()
589
590 if not self.changes:
591 self.get_changes()
592
593 referenced_issue_uids = set(itertools.chain.from_iterable(
594 change['bugs'] for change in self.changes))
595 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
596 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
597
598 missing_issues_by_project = collections.defaultdict(list)
599 for issue_uid in missing_issue_uids:
600 project, issue_id = issue_uid.split(':')
601 missing_issues_by_project[project].append(issue_id)
602
603 for project, issue_ids in missing_issues_by_project.iteritems():
604 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000605
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000606 def print_issues(self):
607 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000608 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000609 for issue in self.issues:
610 self.print_issue(issue)
611
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100612 def print_changes_by_issue(self, skip_empty_own):
613 if not self.issues or not self.changes:
614 return
615
616 self.print_heading('Changes by referenced issue(s)')
617 issues = {issue['uid']: issue for issue in self.issues}
618 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
619 changes_by_issue_uid = collections.defaultdict(list)
620 changes_by_ref_issue_uid = collections.defaultdict(list)
621 changes_without_issue = []
622 for change in self.changes:
623 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000624 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100625 if issue_uid in issues:
626 changes_by_issue_uid[issue_uid].append(change)
627 added = True
628 if issue_uid in ref_issues:
629 changes_by_ref_issue_uid[issue_uid].append(change)
630 added = True
631 if not added:
632 changes_without_issue.append(change)
633
634 # Changes referencing own issues.
635 for issue_uid in issues:
636 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
637 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000638 if changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000639 print()
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000640 for change in changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000641 print(' ', end='') # this prints no newline
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000642 self.print_change(change)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000643 print()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100644
645 # Changes referencing others' issues.
646 for issue_uid in ref_issues:
647 assert changes_by_ref_issue_uid[issue_uid]
648 self.print_issue(ref_issues[issue_uid])
649 for change in changes_by_ref_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000650 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100651 self.print_change(change)
652
653 # Changes referencing no issues.
654 if changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000655 print(self.options.output_format_no_url.format(title='Other changes'))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100656 for change in changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000657 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100658 self.print_change(change)
659
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000660 def print_activity(self):
661 self.print_changes()
662 self.print_reviews()
663 self.print_issues()
664
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000665 def dump_json(self, ignore_keys=None):
666 if ignore_keys is None:
667 ignore_keys = ['replies']
668
669 def format_for_json_dump(in_array):
670 output = {}
671 for item in in_array:
672 url = item.get('url') or item.get('review_url')
673 if not url:
674 raise Exception('Dumped item %s does not specify url' % item)
675 output[url] = dict(
676 (k, v) for k,v in item.iteritems() if k not in ignore_keys)
677 return output
678
679 class PythonObjectEncoder(json.JSONEncoder):
680 def default(self, obj): # pylint: disable=method-hidden
681 if isinstance(obj, datetime):
682 return obj.isoformat()
683 if isinstance(obj, set):
684 return list(obj)
685 return json.JSONEncoder.default(self, obj)
686
687 output = {
688 'reviews': format_for_json_dump(self.reviews),
689 'changes': format_for_json_dump(self.changes),
690 'issues': format_for_json_dump(self.issues)
691 }
Raul Tambre80ee78e2019-05-06 22:41:05 +0000692 print(json.dumps(output, indent=2, cls=PythonObjectEncoder))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000693
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000694
695def main():
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000696 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
697 parser.add_option(
698 '-u', '--user', metavar='<email>',
Bruce Dawson84370962019-02-21 05:33:37 +0000699 # Look for USER and USERNAME (Windows) environment variables.
700 default=os.environ.get('USER', os.environ.get('USERNAME')),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000701 help='Filter on user, default=%default')
702 parser.add_option(
703 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000704 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000705 parser.add_option(
706 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000707 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000708 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
709 relativedelta(months=2))
710 parser.add_option(
711 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000712 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000713 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
714 parser.add_option(
715 '-Y', '--this_year', action='store_true',
716 help='Use this year\'s dates')
717 parser.add_option(
718 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000719 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000720 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000721 '-W', '--last_week', action='count',
722 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000723 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000724 '-a', '--auth',
725 action='store_true',
726 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000727 parser.add_option(
728 '-d', '--deltas',
729 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800730 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100731 parser.add_option(
732 '--no-referenced-issues',
733 action='store_true',
734 help='Do not fetch issues referenced by owned changes. Useful in '
735 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100736 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100737 parser.add_option(
738 '--skip-own-issues-without-changes',
739 action='store_true',
740 help='Skips listing own issues without changes when showing changes '
741 'grouped by referenced issue(s). See --changes-by-issue for more '
742 'details.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000743
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000744 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000745 'By default, all activity will be looked up and '
746 'printed. If any of these are specified, only '
747 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000748 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000749 '-c', '--changes',
750 action='store_true',
751 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000752 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000753 '-i', '--issues',
754 action='store_true',
755 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000756 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000757 '-r', '--reviews',
758 action='store_true',
759 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100760 activity_types_group.add_option(
761 '--changes-by-issue', action='store_true',
762 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000763 parser.add_option_group(activity_types_group)
764
765 output_format_group = optparse.OptionGroup(parser, 'Output Format',
766 'By default, all activity will be printed in the '
767 'following format: {url} {title}. This can be '
768 'changed for either all activity types or '
769 'individually for each activity type. The format '
770 'is defined as documented for '
771 'string.format(...). The variables available for '
Lutz Justen860378e2019-03-12 01:10:02 +0000772 'all activity types are url, title, author, '
773 'created and modified. Format options for '
774 'specific activity types will override the '
775 'generic format.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000776 output_format_group.add_option(
777 '-f', '--output-format', metavar='<format>',
778 default=u'{url} {title}',
779 help='Specifies the format to use when printing all your activity.')
780 output_format_group.add_option(
781 '--output-format-changes', metavar='<format>',
782 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000783 help='Specifies the format to use when printing changes. Supports the '
784 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000785 output_format_group.add_option(
786 '--output-format-issues', metavar='<format>',
787 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000788 help='Specifies the format to use when printing issues. Supports the '
789 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000790 output_format_group.add_option(
791 '--output-format-reviews', metavar='<format>',
792 default=None,
793 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000794 output_format_group.add_option(
795 '--output-format-heading', metavar='<format>',
796 default=u'{heading}:',
797 help='Specifies the format to use when printing headings.')
798 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100799 '--output-format-no-url', default='{title}',
800 help='Specifies the format to use when printing activity without url.')
801 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000802 '-m', '--markdown', action='store_true',
803 help='Use markdown-friendly output (overrides --output-format '
804 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000805 output_format_group.add_option(
806 '-j', '--json', action='store_true',
807 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000808 parser.add_option_group(output_format_group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000809 auth.add_auth_options(parser)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000810
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000811 parser.add_option(
812 '-v', '--verbose',
813 action='store_const',
814 dest='verbosity',
815 default=logging.WARN,
816 const=logging.INFO,
817 help='Output extra informational messages.'
818 )
819 parser.add_option(
820 '-q', '--quiet',
821 action='store_const',
822 dest='verbosity',
823 const=logging.ERROR,
824 help='Suppress non-error messages.'
825 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000826 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000827 '-M', '--merged-only',
828 action='store_true',
829 dest='merged_only',
830 default=False,
831 help='Shows only changes that have been merged.')
832 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000833 '-C', '--completed-issues',
834 action='store_true',
835 dest='completed_issues',
836 default=False,
837 help='Shows only monorail issues that have completed (Fixed|Verified) '
838 'by the user.')
839 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000840 '-o', '--output', metavar='<file>',
841 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000842
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000843 # Remove description formatting
844 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800845 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000846
847 options, args = parser.parse_args()
848 options.local_user = os.environ.get('USER')
849 if args:
850 parser.error('Args unsupported')
851 if not options.user:
Bruce Dawson84370962019-02-21 05:33:37 +0000852 parser.error('USER/USERNAME is not set, please use -u')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000853 options.user = username(options.user)
854
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000855 logging.basicConfig(level=options.verbosity)
856
857 # python-keyring provides easy access to the system keyring.
858 try:
859 import keyring # pylint: disable=unused-import,unused-variable,F0401
860 except ImportError:
861 logging.warning('Consider installing python-keyring')
862
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000863 if not options.begin:
864 if options.last_quarter:
865 begin, end = quarter_begin, quarter_end
866 elif options.this_year:
867 begin, end = get_year_of(datetime.today())
868 elif options.week_of:
869 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000870 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000871 begin, end = (get_week_of(datetime.today() -
872 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000873 else:
874 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
875 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700876 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000877 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700878 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000879 else:
880 end = datetime.today()
881 options.begin, options.end = begin, end
Bruce Dawson2afcf222019-02-25 22:07:08 +0000882 if begin >= end:
883 # The queries fail in peculiar ways when the begin date is in the future.
884 # Give a descriptive error message instead.
885 logging.error('Start date (%s) is the same or later than end date (%s)' %
886 (begin, end))
887 return 1
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000888
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000889 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000890 options.output_format_heading = '### {heading}\n'
891 options.output_format = ' * [{title}]({url})'
892 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000893 logging.info('Searching for activity by %s', options.user)
894 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000895
896 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100897 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000898
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100899 if not (options.changes or options.reviews or options.issues or
900 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000901 options.changes = True
902 options.issues = True
903 options.reviews = True
904
905 # First do any required authentication so none of the user interaction has to
906 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100907 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000908 my_activity.auth_for_changes()
909 if options.reviews:
910 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000911
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000912 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000913
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000914 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100915 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000916 my_activity.get_changes()
917 if options.reviews:
918 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100919 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000920 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100921 if not options.no_referenced_issues:
922 my_activity.get_referenced_issues()
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000923 except auth.AuthenticationError as e:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000924 logging.error('auth.AuthenticationError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000925
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100926 my_activity.show_progress('\n')
927
Vadim Bendebury8de38002018-05-14 19:02:55 -0700928 my_activity.print_access_errors()
929
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000930 output_file = None
931 try:
932 if options.output:
933 output_file = open(options.output, 'w')
934 logging.info('Printing output to "%s"', options.output)
935 sys.stdout = output_file
936 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700937 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000938 else:
939 if options.json:
940 my_activity.dump_json()
941 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100942 if options.changes:
943 my_activity.print_changes()
944 if options.reviews:
945 my_activity.print_reviews()
946 if options.issues:
947 my_activity.print_issues()
948 if options.changes_by_issue:
949 my_activity.print_changes_by_issue(
950 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000951 finally:
952 if output_file:
953 logging.info('Done printing to file.')
954 sys.stdout = sys.__stdout__
955 output_file.close()
956
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000957 return 0
958
959
960if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000961 # Fix encoding to support non-ascii issue titles.
962 fix_encoding.fix_encoding()
963
sbc@chromium.org013731e2015-02-26 18:28:43 +0000964 try:
965 sys.exit(main())
966 except KeyboardInterrupt:
967 sys.stderr.write('interrupted\n')
968 sys.exit(1)