blob: 7940a416d8cf704a7349e9202ce4eb1aca0b4604 [file] [log] [blame]
Gabriel Charettebc6617a2019-02-05 21:30:52 +00001#!/usr/bin/env vpython
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Get stats about your activity.
7
8Example:
9 - my_activity.py for stats for the current week (last week on mondays).
10 - my_activity.py -Q for stats for last quarter.
11 - my_activity.py -Y for stats for this year.
Mohamed Heikaldc37feb2019-06-28 22:33:58 +000012 - my_activity.py -b 4/24/19 for stats since April 24th 2019.
13 - my_activity.py -b 4/24/19 -e 6/16/19 stats between April 24th and June 16th.
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +000014 - my_activity.py -jd to output stats for the week to json with deltas data.
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000015"""
16
17# These services typically only provide a created time and a last modified time
18# for each item for general queries. This is not enough to determine if there
19# was activity in a given time period. So, we first query for all things created
20# before end and modified after begin. Then, we get the details of each item and
21# check those details to determine if there was activity in the given period.
22# This means that query time scales mostly with (today() - begin).
23
Raul Tambre80ee78e2019-05-06 22:41:05 +000024from __future__ import print_function
25
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010026import collections
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +010027import contextlib
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000028from datetime import datetime
29from datetime import timedelta
Edward Lemur202c5592019-10-21 22:44:52 +000030import httplib2
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010031import itertools
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000032import json
Tobias Sargeantffb3c432017-03-08 14:09:14 +000033import logging
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +010034from multiprocessing.pool import ThreadPool
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000035import optparse
36import os
37import subprocess
Tobias Sargeantffb3c432017-03-08 14:09:14 +000038from string import Formatter
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000039import sys
40import urllib
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +000041import re
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000042
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
stevefung@chromium.org832d51e2015-05-27 12:52:51 +000044import fix_encoding
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000045import gerrit_util
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000046
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +000047
Edward Lemur2a048032020-01-14 22:58:13 +000048if sys.version_info.major == 2:
49 import urllib as urllib_parse
50else:
51 import urllib.parse as urllib_parse
52
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000053try:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000054 import dateutil # pylint: disable=import-error
55 import dateutil.parser
56 from dateutil.relativedelta import relativedelta
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000057except ImportError:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000058 logging.error('python-dateutil package required')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000059 exit(1)
60
Tobias Sargeantffb3c432017-03-08 14:09:14 +000061
62class DefaultFormatter(Formatter):
63 def __init__(self, default = ''):
64 super(DefaultFormatter, self).__init__()
65 self.default = default
66
67 def get_value(self, key, args, kwds):
Edward Lemur2a048032020-01-14 22:58:13 +000068 if isinstance(key, str) and key not in kwds:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000069 return self.default
70 return Formatter.get_value(self, key, args, kwds)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000071
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000072gerrit_instances = [
73 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000074 'url': 'android-review.googlesource.com',
Mike Frysingerfdf027a2020-02-27 21:53:45 +000075 'shorturl': 'r.android.com',
76 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000077 },
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000078 {
79 'url': 'chrome-internal-review.googlesource.com',
Jeremy Romanf475a472017-06-06 16:49:11 -040080 'shorturl': 'crrev.com/i',
Varun Khanejad9f97bc2017-08-02 12:55:01 -070081 'short_url_protocol': 'https',
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000082 },
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000083 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000084 'url': 'chromium-review.googlesource.com',
85 'shorturl': 'crrev.com/c',
86 'short_url_protocol': 'https',
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000087 },
Ryan Harrison897602a2017-09-18 16:23:41 -040088 {
Ryan Harrison06e18692019-09-23 18:22:25 +000089 'url': 'dawn-review.googlesource.com',
90 },
91 {
Ryan Harrison897602a2017-09-18 16:23:41 -040092 'url': 'pdfium-review.googlesource.com',
93 },
Adrienne Walker95d4c852018-09-27 20:28:12 +000094 {
95 'url': 'skia-review.googlesource.com',
96 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000097]
98
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010099monorail_projects = {
Jamie Madill1db68ea2019-09-03 19:34:42 +0000100 'angleproject': {
101 'shorturl': 'anglebug.com',
102 'short_url_protocol': 'http',
103 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100104 'chromium': {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000105 'shorturl': 'crbug.com',
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700106 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000107 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000108 'dawn': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100109 'google-breakpad': {},
110 'gyp': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100111 'pdfium': {
Ryan Harrison897602a2017-09-18 16:23:41 -0400112 'shorturl': 'crbug.com/pdfium',
113 'short_url_protocol': 'https',
114 },
Ryan Harrison06e18692019-09-23 18:22:25 +0000115 'skia': {},
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100116 'v8': {
117 'shorturl': 'crbug.com/v8',
118 'short_url_protocol': 'https',
119 },
120}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000121
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000122def username(email):
123 """Keeps the username of an email address."""
124 return email and email.split('@', 1)[0]
125
126
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000127def datetime_to_midnight(date):
128 return date - timedelta(hours=date.hour, minutes=date.minute,
129 seconds=date.second, microseconds=date.microsecond)
130
131
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000132def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000133 begin = (datetime_to_midnight(date) -
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000134 relativedelta(months=(date.month - 1) % 3, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000135 return begin, begin + relativedelta(months=3)
136
137
138def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000139 begin = (datetime_to_midnight(date) -
140 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000141 return begin, begin + relativedelta(years=1)
142
143
144def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000145 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000146 return begin, begin + timedelta(days=7)
147
148
149def get_yes_or_no(msg):
150 while True:
151 response = raw_input(msg + ' yes/no [no] ')
152 if response == 'y' or response == 'yes':
153 return True
154 elif not response or response == 'n' or response == 'no':
155 return False
156
157
deymo@chromium.org6c039202013-09-12 12:28:12 +0000158def datetime_from_gerrit(date_string):
159 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
160
161
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100162def datetime_from_monorail(date_string):
163 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000164
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000165def extract_bug_numbers_from_description(issue):
166 # Getting the description for REST Gerrit
167 revision = issue['revisions'][issue['current_revision']]
168 description = revision['commit']['message']
169
170 bugs = []
171 # Handle both "Bug: 99999" and "BUG=99999" bug notations
172 # Multiple bugs can be noted on a single line or in multiple ones.
173 matches = re.findall(
174 r'^(BUG=|(Bug|Fixed):\s*)((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)',
175 description, flags=re.IGNORECASE | re.MULTILINE)
176 if matches:
177 for match in matches:
178 bugs.extend(match[2].replace(' ', '').split(','))
179 # Add default chromium: prefix if none specified.
180 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
181
182 return sorted(set(bugs))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000183
184class MyActivity(object):
185 def __init__(self, options):
186 self.options = options
187 self.modified_after = options.begin
188 self.modified_before = options.end
189 self.user = options.user
190 self.changes = []
191 self.reviews = []
192 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100193 self.referenced_issues = []
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000194 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-14 19:02:55 -0700195 self.access_errors = set()
Vadim Bendebury80012972019-11-23 02:23:11 +0000196 self.skip_servers = (options.skip_servers.split(','))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000197
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100198 def show_progress(self, how='.'):
199 if sys.stdout.isatty():
200 sys.stdout.write(how)
201 sys.stdout.flush()
202
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):
Vadim Bendebury80012972019-11-23 02:23:11 +0000219 if instance['url'] in self.skip_servers:
220 return []
deymo@chromium.org6c039202013-09-12 12:28:12 +0000221 max_age = datetime.today() - self.modified_after
Andrii Shyshkalov4aedec02018-09-17 16:31:17 +0000222 filters = ['-age:%ss' % (max_age.days * 24 * 3600 + max_age.seconds)]
223 if owner:
224 assert not reviewer
225 filters.append('owner:%s' % owner)
226 else:
227 filters.extend(('-owner:%s' % reviewer, 'reviewer:%s' % reviewer))
Peter K. Lee9342ac02018-08-28 21:03:51 +0000228 # TODO(cjhopman): Should abandoned changes be filtered out when
229 # merged_only is not enabled?
230 if self.options.merged_only:
231 filters.append('status:merged')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000232
Aaron Gable2979a872017-09-05 17:38:32 -0700233 issues = self.gerrit_changes_over_rest(instance, filters)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100234 self.show_progress()
Aaron Gable2979a872017-09-05 17:38:32 -0700235 issues = [self.process_gerrit_issue(instance, issue)
236 for issue in issues]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000237
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000238 issues = filter(self.filter_issue, issues)
239 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
240
241 return issues
242
Aaron Gable2979a872017-09-05 17:38:32 -0700243 def process_gerrit_issue(self, instance, issue):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000244 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000245 if self.options.deltas:
246 ret['delta'] = DefaultFormatter().format(
247 '+{insertions},-{deletions}',
248 **issue)
249 ret['status'] = issue['status']
deymo@chromium.org6c039202013-09-12 12:28:12 +0000250 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700251 protocol = instance.get('short_url_protocol', 'http')
252 url = instance['shorturl']
253 else:
254 protocol = 'https'
255 url = instance['url']
256 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
257
deymo@chromium.org6c039202013-09-12 12:28:12 +0000258 ret['header'] = issue['subject']
Don Garrett2ebf9fd2018-08-06 15:14:00 +0000259 ret['owner'] = issue['owner'].get('email', '')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000260 ret['author'] = ret['owner']
261 ret['created'] = datetime_from_gerrit(issue['created'])
262 ret['modified'] = datetime_from_gerrit(issue['updated'])
263 if 'messages' in issue:
Aaron Gable2979a872017-09-05 17:38:32 -0700264 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
deymo@chromium.org6c039202013-09-12 12:28:12 +0000265 else:
266 ret['replies'] = []
267 ret['reviewers'] = set(r['author'] for r in ret['replies'])
268 ret['reviewers'].discard(ret['author'])
Edward Lemurab9cc2c2020-02-25 23:26:24 +0000269 ret['bugs'] = extract_bug_numbers_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000270 return ret
271
272 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700273 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000274 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000275 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
276 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000277 for reply in replies:
278 ret.append({
279 'author': reply['author']['email'],
280 'created': datetime_from_gerrit(reply['date']),
281 'content': reply['message'],
282 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000283 return ret
284
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100285 def monorail_get_auth_http(self):
Kenneth Russell66badbd2018-09-09 21:35:32 +0000286 # Manually use a long timeout (10m); for some users who have a
287 # long history on the issue tracker, whatever the default timeout
288 # is is reached.
Edward Lemur5b929a42019-10-21 17:57:39 +0000289 return auth.Authenticator().authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100290
291 def filter_modified_monorail_issue(self, issue):
292 """Precisely checks if an issue has been modified in the time range.
293
294 This fetches all issue comments to check if the issue has been modified in
295 the time range specified by user. This is needed because monorail only
296 allows filtering by last updated and published dates, which is not
297 sufficient to tell whether a given issue has been modified at some specific
298 time range. Any update to the issue is a reported as comment on Monorail.
299
300 Args:
301 issue: Issue dict as returned by monorail_query_issues method. In
302 particular, must have a key 'uid' formatted as 'project:issue_id'.
303
304 Returns:
305 Passed issue if modified, None otherwise.
306 """
307 http = self.monorail_get_auth_http()
308 project, issue_id = issue['uid'].split(':')
309 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
310 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
311 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100312 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100313 content = json.loads(body)
314 if not content:
315 logging.error('Unable to parse %s response from monorail.', project)
316 return issue
317
318 for item in content.get('items', []):
319 comment_published = datetime_from_monorail(item['published'])
320 if self.filter_modified(comment_published):
321 return issue
322
323 return None
324
325 def monorail_query_issues(self, project, query):
326 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000327 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100328 '/%s/issues') % project
Edward Lemur2a048032020-01-14 22:58:13 +0000329 query_data = urllib_parse.urlencode(query)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100330 url = url + '?' + query_data
331 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100332 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100333 content = json.loads(body)
334 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100335 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100336 return []
337
338 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100339 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100340 for item in content.get('items', []):
341 if project_config.get('shorturl'):
342 protocol = project_config.get('short_url_protocol', 'http')
343 item_url = '%s://%s/%d' % (
344 protocol, project_config['shorturl'], item['id'])
345 else:
346 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
347 project, item['id'])
348 issue = {
349 'uid': '%s:%s' % (project, item['id']),
350 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100351 'created': datetime_from_monorail(item['published']),
352 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100353 'author': item['author']['name'],
354 'url': item_url,
355 'comments': [],
356 'status': item['status'],
357 'labels': [],
358 'components': []
359 }
360 if 'owner' in item:
361 issue['owner'] = item['owner']['name']
362 else:
363 issue['owner'] = 'None'
364 if 'labels' in item:
365 issue['labels'] = item['labels']
366 if 'components' in item:
367 issue['components'] = item['components']
368 issues.append(issue)
369
370 return issues
371
372 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000373 epoch = datetime.utcfromtimestamp(0)
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000374 # Defaults to @chromium.org email if one wasn't provided on -u option.
375 user_str = (self.options.email if self.options.email.find('@') >= 0
376 else '%s@chromium.org' % self.user)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000377
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100378 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000379 'maxResults': 10000,
380 'q': user_str,
381 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
382 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000383 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000384
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000385 if self.options.completed_issues:
386 return [
387 issue for issue in issues
388 if (self.match(issue['owner']) and
389 issue['status'].lower() in ('verified', 'fixed'))
390 ]
391
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100392 return [
393 issue for issue in issues
394 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000395
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100396 def monorail_get_issues(self, project, issue_ids):
397 return self.monorail_query_issues(project, {
398 'maxResults': 10000,
399 'q': 'id:%s' % ','.join(issue_ids)
400 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000401
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000402 def print_heading(self, heading):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000403 print()
404 print(self.options.output_format_heading.format(heading=heading))
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000405
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000406 def match(self, author):
407 if '@' in self.user:
408 return author == self.user
409 return author.startswith(self.user + '@')
410
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000411 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000412 activity = len([
413 reply
414 for reply in change['replies']
415 if self.match(reply['author'])
416 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000417 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000418 'created': change['created'].date().isoformat(),
419 'modified': change['modified'].date().isoformat(),
420 'reviewers': ', '.join(change['reviewers']),
421 'status': change['status'],
422 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000423 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000424 if self.options.deltas:
425 optional_values['delta'] = change['delta']
426
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000427 self.print_generic(self.options.output_format,
428 self.options.output_format_changes,
429 change['header'],
430 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000431 change['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000432 change['created'],
433 change['modified'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000434 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000435
436 def print_issue(self, issue):
437 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000438 'created': issue['created'].date().isoformat(),
439 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000440 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000441 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000442 }
443 self.print_generic(self.options.output_format,
444 self.options.output_format_issues,
445 issue['header'],
446 issue['url'],
447 issue['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000448 issue['created'],
449 issue['modified'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000450 optional_values)
451
452 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000453 activity = len([
454 reply
455 for reply in review['replies']
456 if self.match(reply['author'])
457 ])
458 optional_values = {
459 'created': review['created'].date().isoformat(),
460 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800461 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000462 'activity': activity,
463 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800464 if self.options.deltas:
465 optional_values['delta'] = review['delta']
466
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000467 self.print_generic(self.options.output_format,
468 self.options.output_format_reviews,
469 review['header'],
470 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000471 review['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000472 review['created'],
473 review['modified'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000474 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000475
476 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000477 def print_generic(default_fmt, specific_fmt,
Lutz Justen860378e2019-03-12 01:10:02 +0000478 title, url, author, created, modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000479 optional_values=None):
480 output_format = specific_fmt if specific_fmt is not None else default_fmt
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000481 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000482 'title': title,
483 'url': url,
484 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000485 'created': created,
486 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000487 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000488 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000489 values.update(optional_values)
Edward Lemur2a048032020-01-14 22:58:13 +0000490 print(DefaultFormatter().format(output_format, **values))
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(
Vadim Bendebury80012972019-11-23 02:23:11 +0000728 '--skip_servers',
729 action='store',
730 default='',
731 help='A comma separated list of gerrit and rietveld servers to ignore')
732 parser.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100733 '--skip-own-issues-without-changes',
734 action='store_true',
735 help='Skips listing own issues without changes when showing changes '
736 'grouped by referenced issue(s). See --changes-by-issue for more '
737 'details.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000738
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000739 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000740 'By default, all activity will be looked up and '
741 'printed. If any of these are specified, only '
742 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000743 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000744 '-c', '--changes',
745 action='store_true',
746 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000747 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000748 '-i', '--issues',
749 action='store_true',
750 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000751 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000752 '-r', '--reviews',
753 action='store_true',
754 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100755 activity_types_group.add_option(
756 '--changes-by-issue', action='store_true',
757 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000758 parser.add_option_group(activity_types_group)
759
760 output_format_group = optparse.OptionGroup(parser, 'Output Format',
761 'By default, all activity will be printed in the '
762 'following format: {url} {title}. This can be '
763 'changed for either all activity types or '
764 'individually for each activity type. The format '
765 'is defined as documented for '
766 'string.format(...). The variables available for '
Lutz Justen860378e2019-03-12 01:10:02 +0000767 'all activity types are url, title, author, '
768 'created and modified. Format options for '
769 'specific activity types will override the '
770 'generic format.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000771 output_format_group.add_option(
772 '-f', '--output-format', metavar='<format>',
773 default=u'{url} {title}',
774 help='Specifies the format to use when printing all your activity.')
775 output_format_group.add_option(
776 '--output-format-changes', metavar='<format>',
777 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000778 help='Specifies the format to use when printing changes. Supports the '
779 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000780 output_format_group.add_option(
781 '--output-format-issues', metavar='<format>',
782 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000783 help='Specifies the format to use when printing issues. Supports the '
784 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000785 output_format_group.add_option(
786 '--output-format-reviews', metavar='<format>',
787 default=None,
788 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000789 output_format_group.add_option(
790 '--output-format-heading', metavar='<format>',
791 default=u'{heading}:',
792 help='Specifies the format to use when printing headings.')
793 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100794 '--output-format-no-url', default='{title}',
795 help='Specifies the format to use when printing activity without url.')
796 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000797 '-m', '--markdown', action='store_true',
798 help='Use markdown-friendly output (overrides --output-format '
799 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000800 output_format_group.add_option(
801 '-j', '--json', action='store_true',
802 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000803 parser.add_option_group(output_format_group)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000804
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000805 parser.add_option(
806 '-v', '--verbose',
807 action='store_const',
808 dest='verbosity',
809 default=logging.WARN,
810 const=logging.INFO,
811 help='Output extra informational messages.'
812 )
813 parser.add_option(
814 '-q', '--quiet',
815 action='store_const',
816 dest='verbosity',
817 const=logging.ERROR,
818 help='Suppress non-error messages.'
819 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000820 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000821 '-M', '--merged-only',
822 action='store_true',
823 dest='merged_only',
824 default=False,
825 help='Shows only changes that have been merged.')
826 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000827 '-C', '--completed-issues',
828 action='store_true',
829 dest='completed_issues',
830 default=False,
831 help='Shows only monorail issues that have completed (Fixed|Verified) '
832 'by the user.')
833 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000834 '-o', '--output', metavar='<file>',
835 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000836
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000837 # Remove description formatting
838 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800839 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000840
841 options, args = parser.parse_args()
842 options.local_user = os.environ.get('USER')
843 if args:
844 parser.error('Args unsupported')
845 if not options.user:
Bruce Dawson84370962019-02-21 05:33:37 +0000846 parser.error('USER/USERNAME is not set, please use -u')
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000847 # Retains the original -u option as the email address.
848 options.email = options.user
849 options.user = username(options.email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000850
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000851 logging.basicConfig(level=options.verbosity)
852
853 # python-keyring provides easy access to the system keyring.
854 try:
855 import keyring # pylint: disable=unused-import,unused-variable,F0401
856 except ImportError:
857 logging.warning('Consider installing python-keyring')
858
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000859 if not options.begin:
860 if options.last_quarter:
861 begin, end = quarter_begin, quarter_end
862 elif options.this_year:
863 begin, end = get_year_of(datetime.today())
864 elif options.week_of:
865 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000866 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000867 begin, end = (get_week_of(datetime.today() -
868 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000869 else:
870 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
871 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700872 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000873 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700874 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000875 else:
876 end = datetime.today()
877 options.begin, options.end = begin, end
Bruce Dawson2afcf222019-02-25 22:07:08 +0000878 if begin >= end:
879 # The queries fail in peculiar ways when the begin date is in the future.
880 # Give a descriptive error message instead.
881 logging.error('Start date (%s) is the same or later than end date (%s)' %
882 (begin, end))
883 return 1
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000884
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000885 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000886 options.output_format_heading = '### {heading}\n'
887 options.output_format = ' * [{title}]({url})'
888 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000889 logging.info('Searching for activity by %s', options.user)
890 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000891
892 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100893 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000894
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100895 if not (options.changes or options.reviews or options.issues or
896 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000897 options.changes = True
898 options.issues = True
899 options.reviews = True
900
901 # First do any required authentication so none of the user interaction has to
902 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100903 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000904 my_activity.auth_for_changes()
905 if options.reviews:
906 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000907
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000908 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000909
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000910 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100911 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000912 my_activity.get_changes()
913 if options.reviews:
914 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100915 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000916 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100917 if not options.no_referenced_issues:
918 my_activity.get_referenced_issues()
Edward Lemur5b929a42019-10-21 17:57:39 +0000919 except auth.LoginRequiredError as e:
920 logging.error('auth.LoginRequiredError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000921
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100922 my_activity.show_progress('\n')
923
Vadim Bendebury8de38002018-05-14 19:02:55 -0700924 my_activity.print_access_errors()
925
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000926 output_file = None
927 try:
928 if options.output:
929 output_file = open(options.output, 'w')
930 logging.info('Printing output to "%s"', options.output)
931 sys.stdout = output_file
932 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700933 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000934 else:
935 if options.json:
936 my_activity.dump_json()
937 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100938 if options.changes:
939 my_activity.print_changes()
940 if options.reviews:
941 my_activity.print_reviews()
942 if options.issues:
943 my_activity.print_issues()
944 if options.changes_by_issue:
945 my_activity.print_changes_by_issue(
946 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000947 finally:
948 if output_file:
949 logging.info('Done printing to file.')
950 sys.stdout = sys.__stdout__
951 output_file.close()
952
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000953 return 0
954
955
956if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000957 # Fix encoding to support non-ascii issue titles.
958 fix_encoding.fix_encoding()
959
sbc@chromium.org013731e2015-02-26 18:28:43 +0000960 try:
961 sys.exit(main())
962 except KeyboardInterrupt:
963 sys.stderr.write('interrupted\n')
964 sys.exit(1)