blob: 164ee504273af8cc3832be255d8b441a8928949a [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
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000048try:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000049 import dateutil # pylint: disable=import-error
50 import dateutil.parser
51 from dateutil.relativedelta import relativedelta
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000052except ImportError:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000053 logging.error('python-dateutil package required')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000054 exit(1)
55
Tobias Sargeantffb3c432017-03-08 14:09:14 +000056
57class DefaultFormatter(Formatter):
58 def __init__(self, default = ''):
59 super(DefaultFormatter, self).__init__()
60 self.default = default
61
62 def get_value(self, key, args, kwds):
63 if isinstance(key, basestring) and key not in kwds:
64 return self.default
65 return Formatter.get_value(self, key, args, kwds)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000066
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000067gerrit_instances = [
68 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000069 'url': 'android-review.googlesource.com',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000070 },
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000071 {
72 'url': 'chrome-internal-review.googlesource.com',
Jeremy Romanf475a472017-06-06 16:49:11 -040073 'shorturl': 'crrev.com/i',
Varun Khanejad9f97bc2017-08-02 12:55:01 -070074 'short_url_protocol': 'https',
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000075 },
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000076 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000077 'url': 'chromium-review.googlesource.com',
78 'shorturl': 'crrev.com/c',
79 'short_url_protocol': 'https',
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000080 },
Ryan Harrison897602a2017-09-18 16:23:41 -040081 {
Ryan Harrison06e18692019-09-23 18:22:25 +000082 'url': 'dawn-review.googlesource.com',
83 },
84 {
Ryan Harrison897602a2017-09-18 16:23:41 -040085 'url': 'pdfium-review.googlesource.com',
86 },
Adrienne Walker95d4c852018-09-27 20:28:12 +000087 {
88 'url': 'skia-review.googlesource.com',
89 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000090]
91
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010092monorail_projects = {
Jamie Madill1db68ea2019-09-03 19:34:42 +000093 'angleproject': {
94 'shorturl': 'anglebug.com',
95 'short_url_protocol': 'http',
96 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010097 'chromium': {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000098 'shorturl': 'crbug.com',
Varun Khanejad9f97bc2017-08-02 12:55:01 -070099 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000100 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000101 'dawn': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100102 'google-breakpad': {},
103 'gyp': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100104 'pdfium': {
Ryan Harrison897602a2017-09-18 16:23:41 -0400105 'shorturl': 'crbug.com/pdfium',
106 'short_url_protocol': 'https',
107 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000108 'skia': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100109 'v8': {
110 'shorturl': 'crbug.com/v8',
111 'short_url_protocol': 'https',
112 },
113}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000114
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000115def username(email):
116 """Keeps the username of an email address."""
117 return email and email.split('@', 1)[0]
118
119
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000120def datetime_to_midnight(date):
121 return date - timedelta(hours=date.hour, minutes=date.minute,
122 seconds=date.second, microseconds=date.microsecond)
123
124
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000125def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000126 begin = (datetime_to_midnight(date) -
127 relativedelta(months=(date.month % 3) - 1, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000128 return begin, begin + relativedelta(months=3)
129
130
131def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000132 begin = (datetime_to_midnight(date) -
133 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000134 return begin, begin + relativedelta(years=1)
135
136
137def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000138 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000139 return begin, begin + timedelta(days=7)
140
141
142def get_yes_or_no(msg):
143 while True:
144 response = raw_input(msg + ' yes/no [no] ')
145 if response == 'y' or response == 'yes':
146 return True
147 elif not response or response == 'n' or response == 'no':
148 return False
149
150
deymo@chromium.org6c039202013-09-12 12:28:12 +0000151def datetime_from_gerrit(date_string):
152 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
153
154
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100155def datetime_from_monorail(date_string):
156 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000157
158
159class MyActivity(object):
160 def __init__(self, options):
161 self.options = options
162 self.modified_after = options.begin
163 self.modified_before = options.end
164 self.user = options.user
165 self.changes = []
166 self.reviews = []
167 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100168 self.referenced_issues = []
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000169 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-14 19:02:55 -0700170 self.access_errors = set()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000171
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100172 def show_progress(self, how='.'):
173 if sys.stdout.isatty():
174 sys.stdout.write(how)
175 sys.stdout.flush()
176
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000177 def extract_bug_numbers_from_description(self, issue):
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000178 description = None
179
180 if 'description' in issue:
181 # Getting the description for Rietveld
182 description = issue['description']
183 elif 'revisions' in issue:
184 # Getting the description for REST Gerrit
185 revision = issue['revisions'][issue['current_revision']]
186 description = revision['commit']['message']
187
188 bugs = []
189 if description:
Nicolas Dossou-gbete903ea732017-07-10 16:46:59 +0100190 # Handle both "Bug: 99999" and "BUG=99999" bug notations
191 # Multiple bugs can be noted on a single line or in multiple ones.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100192 matches = re.findall(
193 r'BUG[=:]\s?((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)', description,
194 flags=re.IGNORECASE)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000195 if matches:
196 for match in matches:
197 bugs.extend(match[0].replace(' ', '').split(','))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100198 # Add default chromium: prefix if none specified.
199 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000200
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000201 return sorted(set(bugs))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000202
Vadim Bendebury8de38002018-05-14 19:02:55 -0700203 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200204 # Convert the "key:value" filter to a list of (key, value) pairs.
205 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000206 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000207 # Instantiate the generator to force all the requests now and catch the
208 # errors here.
209 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000210 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
211 'CURRENT_REVISION', 'CURRENT_COMMIT']))
Raul Tambre7c938462019-05-24 16:35:35 +0000212 except gerrit_util.GerritError as e:
Vadim Bendebury8de38002018-05-14 19:02:55 -0700213 error_message = 'Looking up %r: %s' % (instance['url'], e)
214 if error_message not in self.access_errors:
215 self.access_errors.add(error_message)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000216 return []
217
deymo@chromium.org6c039202013-09-12 12:28:12 +0000218 def gerrit_search(self, instance, owner=None, reviewer=None):
219 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'])
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000267 ret['bugs'] = self.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
327 query_data = urllib.urlencode(query)
328 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
479 output_format = unicode(output_format)
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000480 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000481 'title': title,
482 'url': url,
483 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000484 'created': created,
485 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000486 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000487 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000488 values.update(optional_values)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000489 print(DefaultFormatter().format(output_format,
490 **values).encode(sys.getdefaultencoding()))
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000491
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000492
493 def filter_issue(self, issue, should_filter_by_user=True):
494 def maybe_filter_username(email):
495 return not should_filter_by_user or username(email) == self.user
496 if (maybe_filter_username(issue['author']) and
497 self.filter_modified(issue['created'])):
498 return True
499 if (maybe_filter_username(issue['owner']) and
500 (self.filter_modified(issue['created']) or
501 self.filter_modified(issue['modified']))):
502 return True
503 for reply in issue['replies']:
504 if self.filter_modified(reply['created']):
505 if not should_filter_by_user:
506 break
507 if (username(reply['author']) == self.user
508 or (self.user + '@') in reply['content']):
509 break
510 else:
511 return False
512 return True
513
514 def filter_modified(self, modified):
515 return self.modified_after < modified and modified < self.modified_before
516
517 def auth_for_changes(self):
518 #TODO(cjhopman): Move authentication check for getting changes here.
519 pass
520
521 def auth_for_reviews(self):
522 # Reviews use all the same instances as changes so no authentication is
523 # required.
524 pass
525
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000526 def get_changes(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000527 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100528 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100529 gerrit_changes = pool.map_async(
530 lambda instance: self.gerrit_search(instance, owner=self.user),
531 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100532 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000533 self.changes = list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000534
535 def print_changes(self):
536 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000537 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000538 for change in self.changes:
Bruce Dawson2afcf222019-02-25 22:07:08 +0000539 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000540
Vadim Bendebury8de38002018-05-14 19:02:55 -0700541 def print_access_errors(self):
542 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400543 logging.error('Access Errors:')
544 for error in self.access_errors:
545 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700546
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000547 def get_reviews(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000548 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100549 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100550 gerrit_reviews = pool.map_async(
551 lambda instance: self.gerrit_search(instance, reviewer=self.user),
552 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100553 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000554 self.reviews = list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000555
556 def print_reviews(self):
557 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000558 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000559 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000560 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000561
562 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100563 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
564 monorail_issues = pool.map(
565 self.monorail_issue_search, monorail_projects.keys())
566 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
567
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700568 if not monorail_issues:
569 return
570
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100571 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
572 filtered_issues = pool.map(
573 self.filter_modified_monorail_issue, monorail_issues)
574 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100575
576 def get_referenced_issues(self):
577 if not self.issues:
578 self.get_issues()
579
580 if not self.changes:
581 self.get_changes()
582
583 referenced_issue_uids = set(itertools.chain.from_iterable(
584 change['bugs'] for change in self.changes))
585 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
586 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
587
588 missing_issues_by_project = collections.defaultdict(list)
589 for issue_uid in missing_issue_uids:
590 project, issue_id = issue_uid.split(':')
591 missing_issues_by_project[project].append(issue_id)
592
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000593 for project, issue_ids in missing_issues_by_project.items():
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100594 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000595
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000596 def print_issues(self):
597 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000598 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000599 for issue in self.issues:
600 self.print_issue(issue)
601
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100602 def print_changes_by_issue(self, skip_empty_own):
603 if not self.issues or not self.changes:
604 return
605
606 self.print_heading('Changes by referenced issue(s)')
607 issues = {issue['uid']: issue for issue in self.issues}
608 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
609 changes_by_issue_uid = collections.defaultdict(list)
610 changes_by_ref_issue_uid = collections.defaultdict(list)
611 changes_without_issue = []
612 for change in self.changes:
613 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000614 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100615 if issue_uid in issues:
616 changes_by_issue_uid[issue_uid].append(change)
617 added = True
618 if issue_uid in ref_issues:
619 changes_by_ref_issue_uid[issue_uid].append(change)
620 added = True
621 if not added:
622 changes_without_issue.append(change)
623
624 # Changes referencing own issues.
625 for issue_uid in issues:
626 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
627 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000628 if changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000629 print()
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000630 for change in changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000631 print(' ', end='') # this prints no newline
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000632 self.print_change(change)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000633 print()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100634
635 # Changes referencing others' issues.
636 for issue_uid in ref_issues:
637 assert changes_by_ref_issue_uid[issue_uid]
638 self.print_issue(ref_issues[issue_uid])
639 for change in changes_by_ref_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000640 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100641 self.print_change(change)
642
643 # Changes referencing no issues.
644 if changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000645 print(self.options.output_format_no_url.format(title='Other changes'))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100646 for change in changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000647 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100648 self.print_change(change)
649
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000650 def print_activity(self):
651 self.print_changes()
652 self.print_reviews()
653 self.print_issues()
654
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000655 def dump_json(self, ignore_keys=None):
656 if ignore_keys is None:
657 ignore_keys = ['replies']
658
659 def format_for_json_dump(in_array):
660 output = {}
661 for item in in_array:
662 url = item.get('url') or item.get('review_url')
663 if not url:
664 raise Exception('Dumped item %s does not specify url' % item)
665 output[url] = dict(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000666 (k, v) for k,v in item.items() if k not in ignore_keys)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000667 return output
668
669 class PythonObjectEncoder(json.JSONEncoder):
670 def default(self, obj): # pylint: disable=method-hidden
671 if isinstance(obj, datetime):
672 return obj.isoformat()
673 if isinstance(obj, set):
674 return list(obj)
675 return json.JSONEncoder.default(self, obj)
676
677 output = {
678 'reviews': format_for_json_dump(self.reviews),
679 'changes': format_for_json_dump(self.changes),
680 'issues': format_for_json_dump(self.issues)
681 }
Raul Tambre80ee78e2019-05-06 22:41:05 +0000682 print(json.dumps(output, indent=2, cls=PythonObjectEncoder))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000683
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000684
685def main():
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000686 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
687 parser.add_option(
688 '-u', '--user', metavar='<email>',
Bruce Dawson84370962019-02-21 05:33:37 +0000689 # Look for USER and USERNAME (Windows) environment variables.
690 default=os.environ.get('USER', os.environ.get('USERNAME')),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000691 help='Filter on user, default=%default')
692 parser.add_option(
693 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000694 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000695 parser.add_option(
696 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000697 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000698 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
699 relativedelta(months=2))
700 parser.add_option(
701 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000702 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000703 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
704 parser.add_option(
705 '-Y', '--this_year', action='store_true',
706 help='Use this year\'s dates')
707 parser.add_option(
708 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000709 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000710 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000711 '-W', '--last_week', action='count',
712 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000713 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000714 '-a', '--auth',
715 action='store_true',
716 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000717 parser.add_option(
718 '-d', '--deltas',
719 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800720 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100721 parser.add_option(
722 '--no-referenced-issues',
723 action='store_true',
724 help='Do not fetch issues referenced by owned changes. Useful in '
725 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100726 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100727 parser.add_option(
728 '--skip-own-issues-without-changes',
729 action='store_true',
730 help='Skips listing own issues without changes when showing changes '
731 'grouped by referenced issue(s). See --changes-by-issue for more '
732 'details.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000733
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000734 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000735 'By default, all activity will be looked up and '
736 'printed. If any of these are specified, only '
737 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000738 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000739 '-c', '--changes',
740 action='store_true',
741 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000742 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000743 '-i', '--issues',
744 action='store_true',
745 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000746 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000747 '-r', '--reviews',
748 action='store_true',
749 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100750 activity_types_group.add_option(
751 '--changes-by-issue', action='store_true',
752 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000753 parser.add_option_group(activity_types_group)
754
755 output_format_group = optparse.OptionGroup(parser, 'Output Format',
756 'By default, all activity will be printed in the '
757 'following format: {url} {title}. This can be '
758 'changed for either all activity types or '
759 'individually for each activity type. The format '
760 'is defined as documented for '
761 'string.format(...). The variables available for '
Lutz Justen860378e2019-03-12 01:10:02 +0000762 'all activity types are url, title, author, '
763 'created and modified. Format options for '
764 'specific activity types will override the '
765 'generic format.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000766 output_format_group.add_option(
767 '-f', '--output-format', metavar='<format>',
768 default=u'{url} {title}',
769 help='Specifies the format to use when printing all your activity.')
770 output_format_group.add_option(
771 '--output-format-changes', metavar='<format>',
772 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000773 help='Specifies the format to use when printing changes. Supports the '
774 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000775 output_format_group.add_option(
776 '--output-format-issues', metavar='<format>',
777 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000778 help='Specifies the format to use when printing issues. Supports the '
779 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000780 output_format_group.add_option(
781 '--output-format-reviews', metavar='<format>',
782 default=None,
783 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000784 output_format_group.add_option(
785 '--output-format-heading', metavar='<format>',
786 default=u'{heading}:',
787 help='Specifies the format to use when printing headings.')
788 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100789 '--output-format-no-url', default='{title}',
790 help='Specifies the format to use when printing activity without url.')
791 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000792 '-m', '--markdown', action='store_true',
793 help='Use markdown-friendly output (overrides --output-format '
794 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000795 output_format_group.add_option(
796 '-j', '--json', action='store_true',
797 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000798 parser.add_option_group(output_format_group)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000799
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000800 parser.add_option(
801 '-v', '--verbose',
802 action='store_const',
803 dest='verbosity',
804 default=logging.WARN,
805 const=logging.INFO,
806 help='Output extra informational messages.'
807 )
808 parser.add_option(
809 '-q', '--quiet',
810 action='store_const',
811 dest='verbosity',
812 const=logging.ERROR,
813 help='Suppress non-error messages.'
814 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000815 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000816 '-M', '--merged-only',
817 action='store_true',
818 dest='merged_only',
819 default=False,
820 help='Shows only changes that have been merged.')
821 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000822 '-C', '--completed-issues',
823 action='store_true',
824 dest='completed_issues',
825 default=False,
826 help='Shows only monorail issues that have completed (Fixed|Verified) '
827 'by the user.')
828 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000829 '-o', '--output', metavar='<file>',
830 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000831
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000832 # Remove description formatting
833 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800834 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000835
836 options, args = parser.parse_args()
837 options.local_user = os.environ.get('USER')
838 if args:
839 parser.error('Args unsupported')
840 if not options.user:
Bruce Dawson84370962019-02-21 05:33:37 +0000841 parser.error('USER/USERNAME is not set, please use -u')
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000842 # Retains the original -u option as the email address.
843 options.email = options.user
844 options.user = username(options.email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000845
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000846 logging.basicConfig(level=options.verbosity)
847
848 # python-keyring provides easy access to the system keyring.
849 try:
850 import keyring # pylint: disable=unused-import,unused-variable,F0401
851 except ImportError:
852 logging.warning('Consider installing python-keyring')
853
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000854 if not options.begin:
855 if options.last_quarter:
856 begin, end = quarter_begin, quarter_end
857 elif options.this_year:
858 begin, end = get_year_of(datetime.today())
859 elif options.week_of:
860 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000861 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000862 begin, end = (get_week_of(datetime.today() -
863 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000864 else:
865 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
866 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700867 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000868 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700869 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000870 else:
871 end = datetime.today()
872 options.begin, options.end = begin, end
Bruce Dawson2afcf222019-02-25 22:07:08 +0000873 if begin >= end:
874 # The queries fail in peculiar ways when the begin date is in the future.
875 # Give a descriptive error message instead.
876 logging.error('Start date (%s) is the same or later than end date (%s)' %
877 (begin, end))
878 return 1
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000879
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000880 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000881 options.output_format_heading = '### {heading}\n'
882 options.output_format = ' * [{title}]({url})'
883 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000884 logging.info('Searching for activity by %s', options.user)
885 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000886
887 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100888 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000889
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100890 if not (options.changes or options.reviews or options.issues or
891 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000892 options.changes = True
893 options.issues = True
894 options.reviews = True
895
896 # First do any required authentication so none of the user interaction has to
897 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100898 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000899 my_activity.auth_for_changes()
900 if options.reviews:
901 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000902
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000903 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000904
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000905 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100906 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000907 my_activity.get_changes()
908 if options.reviews:
909 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100910 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000911 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100912 if not options.no_referenced_issues:
913 my_activity.get_referenced_issues()
Edward Lemur5b929a42019-10-21 17:57:39 +0000914 except auth.LoginRequiredError as e:
915 logging.error('auth.LoginRequiredError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000916
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100917 my_activity.show_progress('\n')
918
Vadim Bendebury8de38002018-05-14 19:02:55 -0700919 my_activity.print_access_errors()
920
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000921 output_file = None
922 try:
923 if options.output:
924 output_file = open(options.output, 'w')
925 logging.info('Printing output to "%s"', options.output)
926 sys.stdout = output_file
927 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700928 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000929 else:
930 if options.json:
931 my_activity.dump_json()
932 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100933 if options.changes:
934 my_activity.print_changes()
935 if options.reviews:
936 my_activity.print_reviews()
937 if options.issues:
938 my_activity.print_issues()
939 if options.changes_by_issue:
940 my_activity.print_changes_by_issue(
941 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000942 finally:
943 if output_file:
944 logging.info('Done printing to file.')
945 sys.stdout = sys.__stdout__
946 output_file.close()
947
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000948 return 0
949
950
951if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000952 # Fix encoding to support non-ascii issue titles.
953 fix_encoding.fix_encoding()
954
sbc@chromium.org013731e2015-02-26 18:28:43 +0000955 try:
956 sys.exit(main())
957 except KeyboardInterrupt:
958 sys.stderr.write('interrupted\n')
959 sys.exit(1)