blob: 8e132f796847f8f4608e1b63b5e51db9cda8c6c3 [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()
Vadim Bendebury80012972019-11-23 02:23:11 +0000171 self.skip_servers = (options.skip_servers.split(','))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000172
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100173 def show_progress(self, how='.'):
174 if sys.stdout.isatty():
175 sys.stdout.write(how)
176 sys.stdout.flush()
177
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000178 def extract_bug_numbers_from_description(self, issue):
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000179 description = None
180
181 if 'description' in issue:
182 # Getting the description for Rietveld
183 description = issue['description']
184 elif 'revisions' in issue:
185 # Getting the description for REST Gerrit
186 revision = issue['revisions'][issue['current_revision']]
187 description = revision['commit']['message']
188
189 bugs = []
190 if description:
Nicolas Dossou-gbete903ea732017-07-10 16:46:59 +0100191 # Handle both "Bug: 99999" and "BUG=99999" bug notations
192 # Multiple bugs can be noted on a single line or in multiple ones.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100193 matches = re.findall(
194 r'BUG[=:]\s?((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)', description,
195 flags=re.IGNORECASE)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000196 if matches:
197 for match in matches:
198 bugs.extend(match[0].replace(' ', '').split(','))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100199 # Add default chromium: prefix if none specified.
200 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000201
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000202 return sorted(set(bugs))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000203
Vadim Bendebury8de38002018-05-14 19:02:55 -0700204 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200205 # Convert the "key:value" filter to a list of (key, value) pairs.
206 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000207 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000208 # Instantiate the generator to force all the requests now and catch the
209 # errors here.
210 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000211 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
212 'CURRENT_REVISION', 'CURRENT_COMMIT']))
Raul Tambre7c938462019-05-24 16:35:35 +0000213 except gerrit_util.GerritError as e:
Vadim Bendebury8de38002018-05-14 19:02:55 -0700214 error_message = 'Looking up %r: %s' % (instance['url'], e)
215 if error_message not in self.access_errors:
216 self.access_errors.add(error_message)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000217 return []
218
deymo@chromium.org6c039202013-09-12 12:28:12 +0000219 def gerrit_search(self, instance, owner=None, reviewer=None):
Vadim Bendebury80012972019-11-23 02:23:11 +0000220 if instance['url'] in self.skip_servers:
221 return []
deymo@chromium.org6c039202013-09-12 12:28:12 +0000222 max_age = datetime.today() - self.modified_after
Andrii Shyshkalov4aedec02018-09-17 16:31:17 +0000223 filters = ['-age:%ss' % (max_age.days * 24 * 3600 + max_age.seconds)]
224 if owner:
225 assert not reviewer
226 filters.append('owner:%s' % owner)
227 else:
228 filters.extend(('-owner:%s' % reviewer, 'reviewer:%s' % reviewer))
Peter K. Lee9342ac02018-08-28 21:03:51 +0000229 # TODO(cjhopman): Should abandoned changes be filtered out when
230 # merged_only is not enabled?
231 if self.options.merged_only:
232 filters.append('status:merged')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000233
Aaron Gable2979a872017-09-05 17:38:32 -0700234 issues = self.gerrit_changes_over_rest(instance, filters)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100235 self.show_progress()
Aaron Gable2979a872017-09-05 17:38:32 -0700236 issues = [self.process_gerrit_issue(instance, issue)
237 for issue in issues]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000238
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000239 issues = filter(self.filter_issue, issues)
240 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
241
242 return issues
243
Aaron Gable2979a872017-09-05 17:38:32 -0700244 def process_gerrit_issue(self, instance, issue):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000245 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000246 if self.options.deltas:
247 ret['delta'] = DefaultFormatter().format(
248 '+{insertions},-{deletions}',
249 **issue)
250 ret['status'] = issue['status']
deymo@chromium.org6c039202013-09-12 12:28:12 +0000251 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700252 protocol = instance.get('short_url_protocol', 'http')
253 url = instance['shorturl']
254 else:
255 protocol = 'https'
256 url = instance['url']
257 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
258
deymo@chromium.org6c039202013-09-12 12:28:12 +0000259 ret['header'] = issue['subject']
Don Garrett2ebf9fd2018-08-06 15:14:00 +0000260 ret['owner'] = issue['owner'].get('email', '')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000261 ret['author'] = ret['owner']
262 ret['created'] = datetime_from_gerrit(issue['created'])
263 ret['modified'] = datetime_from_gerrit(issue['updated'])
264 if 'messages' in issue:
Aaron Gable2979a872017-09-05 17:38:32 -0700265 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
deymo@chromium.org6c039202013-09-12 12:28:12 +0000266 else:
267 ret['replies'] = []
268 ret['reviewers'] = set(r['author'] for r in ret['replies'])
269 ret['reviewers'].discard(ret['author'])
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000270 ret['bugs'] = self.extract_bug_numbers_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000271 return ret
272
273 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700274 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000275 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000276 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
277 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000278 for reply in replies:
279 ret.append({
280 'author': reply['author']['email'],
281 'created': datetime_from_gerrit(reply['date']),
282 'content': reply['message'],
283 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000284 return ret
285
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100286 def monorail_get_auth_http(self):
Kenneth Russell66badbd2018-09-09 21:35:32 +0000287 # Manually use a long timeout (10m); for some users who have a
288 # long history on the issue tracker, whatever the default timeout
289 # is is reached.
Edward Lemur5b929a42019-10-21 17:57:39 +0000290 return auth.Authenticator().authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100291
292 def filter_modified_monorail_issue(self, issue):
293 """Precisely checks if an issue has been modified in the time range.
294
295 This fetches all issue comments to check if the issue has been modified in
296 the time range specified by user. This is needed because monorail only
297 allows filtering by last updated and published dates, which is not
298 sufficient to tell whether a given issue has been modified at some specific
299 time range. Any update to the issue is a reported as comment on Monorail.
300
301 Args:
302 issue: Issue dict as returned by monorail_query_issues method. In
303 particular, must have a key 'uid' formatted as 'project:issue_id'.
304
305 Returns:
306 Passed issue if modified, None otherwise.
307 """
308 http = self.monorail_get_auth_http()
309 project, issue_id = issue['uid'].split(':')
310 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
311 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
312 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100313 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100314 content = json.loads(body)
315 if not content:
316 logging.error('Unable to parse %s response from monorail.', project)
317 return issue
318
319 for item in content.get('items', []):
320 comment_published = datetime_from_monorail(item['published'])
321 if self.filter_modified(comment_published):
322 return issue
323
324 return None
325
326 def monorail_query_issues(self, project, query):
327 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000328 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100329 '/%s/issues') % project
330 query_data = urllib.urlencode(query)
331 url = url + '?' + query_data
332 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100333 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100334 content = json.loads(body)
335 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100336 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100337 return []
338
339 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100340 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100341 for item in content.get('items', []):
342 if project_config.get('shorturl'):
343 protocol = project_config.get('short_url_protocol', 'http')
344 item_url = '%s://%s/%d' % (
345 protocol, project_config['shorturl'], item['id'])
346 else:
347 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
348 project, item['id'])
349 issue = {
350 'uid': '%s:%s' % (project, item['id']),
351 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100352 'created': datetime_from_monorail(item['published']),
353 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100354 'author': item['author']['name'],
355 'url': item_url,
356 'comments': [],
357 'status': item['status'],
358 'labels': [],
359 'components': []
360 }
361 if 'owner' in item:
362 issue['owner'] = item['owner']['name']
363 else:
364 issue['owner'] = 'None'
365 if 'labels' in item:
366 issue['labels'] = item['labels']
367 if 'components' in item:
368 issue['components'] = item['components']
369 issues.append(issue)
370
371 return issues
372
373 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000374 epoch = datetime.utcfromtimestamp(0)
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000375 # Defaults to @chromium.org email if one wasn't provided on -u option.
376 user_str = (self.options.email if self.options.email.find('@') >= 0
377 else '%s@chromium.org' % self.user)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000378
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100379 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000380 'maxResults': 10000,
381 'q': user_str,
382 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
383 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000384 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000385
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000386 if self.options.completed_issues:
387 return [
388 issue for issue in issues
389 if (self.match(issue['owner']) and
390 issue['status'].lower() in ('verified', 'fixed'))
391 ]
392
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100393 return [
394 issue for issue in issues
395 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000396
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100397 def monorail_get_issues(self, project, issue_ids):
398 return self.monorail_query_issues(project, {
399 'maxResults': 10000,
400 'q': 'id:%s' % ','.join(issue_ids)
401 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000402
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000403 def print_heading(self, heading):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000404 print()
405 print(self.options.output_format_heading.format(heading=heading))
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000406
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000407 def match(self, author):
408 if '@' in self.user:
409 return author == self.user
410 return author.startswith(self.user + '@')
411
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000412 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000413 activity = len([
414 reply
415 for reply in change['replies']
416 if self.match(reply['author'])
417 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000418 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000419 'created': change['created'].date().isoformat(),
420 'modified': change['modified'].date().isoformat(),
421 'reviewers': ', '.join(change['reviewers']),
422 'status': change['status'],
423 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000424 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000425 if self.options.deltas:
426 optional_values['delta'] = change['delta']
427
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000428 self.print_generic(self.options.output_format,
429 self.options.output_format_changes,
430 change['header'],
431 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000432 change['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000433 change['created'],
434 change['modified'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000435 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000436
437 def print_issue(self, issue):
438 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000439 'created': issue['created'].date().isoformat(),
440 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000441 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000442 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000443 }
444 self.print_generic(self.options.output_format,
445 self.options.output_format_issues,
446 issue['header'],
447 issue['url'],
448 issue['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000449 issue['created'],
450 issue['modified'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000451 optional_values)
452
453 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000454 activity = len([
455 reply
456 for reply in review['replies']
457 if self.match(reply['author'])
458 ])
459 optional_values = {
460 'created': review['created'].date().isoformat(),
461 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800462 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000463 'activity': activity,
464 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800465 if self.options.deltas:
466 optional_values['delta'] = review['delta']
467
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000468 self.print_generic(self.options.output_format,
469 self.options.output_format_reviews,
470 review['header'],
471 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000472 review['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000473 review['created'],
474 review['modified'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000475 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000476
477 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000478 def print_generic(default_fmt, specific_fmt,
Lutz Justen860378e2019-03-12 01:10:02 +0000479 title, url, author, created, modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000480 optional_values=None):
481 output_format = specific_fmt if specific_fmt is not None else default_fmt
482 output_format = unicode(output_format)
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000483 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000484 'title': title,
485 'url': url,
486 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000487 'created': created,
488 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000489 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000490 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000491 values.update(optional_values)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000492 print(DefaultFormatter().format(output_format,
493 **values).encode(sys.getdefaultencoding()))
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000494
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000495
496 def filter_issue(self, issue, should_filter_by_user=True):
497 def maybe_filter_username(email):
498 return not should_filter_by_user or username(email) == self.user
499 if (maybe_filter_username(issue['author']) and
500 self.filter_modified(issue['created'])):
501 return True
502 if (maybe_filter_username(issue['owner']) and
503 (self.filter_modified(issue['created']) or
504 self.filter_modified(issue['modified']))):
505 return True
506 for reply in issue['replies']:
507 if self.filter_modified(reply['created']):
508 if not should_filter_by_user:
509 break
510 if (username(reply['author']) == self.user
511 or (self.user + '@') in reply['content']):
512 break
513 else:
514 return False
515 return True
516
517 def filter_modified(self, modified):
518 return self.modified_after < modified and modified < self.modified_before
519
520 def auth_for_changes(self):
521 #TODO(cjhopman): Move authentication check for getting changes here.
522 pass
523
524 def auth_for_reviews(self):
525 # Reviews use all the same instances as changes so no authentication is
526 # required.
527 pass
528
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000529 def get_changes(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000530 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100531 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100532 gerrit_changes = pool.map_async(
533 lambda instance: self.gerrit_search(instance, owner=self.user),
534 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100535 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000536 self.changes = list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000537
538 def print_changes(self):
539 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000540 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000541 for change in self.changes:
Bruce Dawson2afcf222019-02-25 22:07:08 +0000542 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000543
Vadim Bendebury8de38002018-05-14 19:02:55 -0700544 def print_access_errors(self):
545 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400546 logging.error('Access Errors:')
547 for error in self.access_errors:
548 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700549
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000550 def get_reviews(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000551 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100552 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100553 gerrit_reviews = pool.map_async(
554 lambda instance: self.gerrit_search(instance, reviewer=self.user),
555 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100556 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000557 self.reviews = list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000558
559 def print_reviews(self):
560 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000561 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000562 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000563 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000564
565 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100566 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
567 monorail_issues = pool.map(
568 self.monorail_issue_search, monorail_projects.keys())
569 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
570
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700571 if not monorail_issues:
572 return
573
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100574 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
575 filtered_issues = pool.map(
576 self.filter_modified_monorail_issue, monorail_issues)
577 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100578
579 def get_referenced_issues(self):
580 if not self.issues:
581 self.get_issues()
582
583 if not self.changes:
584 self.get_changes()
585
586 referenced_issue_uids = set(itertools.chain.from_iterable(
587 change['bugs'] for change in self.changes))
588 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
589 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
590
591 missing_issues_by_project = collections.defaultdict(list)
592 for issue_uid in missing_issue_uids:
593 project, issue_id = issue_uid.split(':')
594 missing_issues_by_project[project].append(issue_id)
595
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000596 for project, issue_ids in missing_issues_by_project.items():
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100597 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000598
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000599 def print_issues(self):
600 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000601 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000602 for issue in self.issues:
603 self.print_issue(issue)
604
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100605 def print_changes_by_issue(self, skip_empty_own):
606 if not self.issues or not self.changes:
607 return
608
609 self.print_heading('Changes by referenced issue(s)')
610 issues = {issue['uid']: issue for issue in self.issues}
611 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
612 changes_by_issue_uid = collections.defaultdict(list)
613 changes_by_ref_issue_uid = collections.defaultdict(list)
614 changes_without_issue = []
615 for change in self.changes:
616 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000617 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100618 if issue_uid in issues:
619 changes_by_issue_uid[issue_uid].append(change)
620 added = True
621 if issue_uid in ref_issues:
622 changes_by_ref_issue_uid[issue_uid].append(change)
623 added = True
624 if not added:
625 changes_without_issue.append(change)
626
627 # Changes referencing own issues.
628 for issue_uid in issues:
629 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
630 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000631 if changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000632 print()
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000633 for change in changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000634 print(' ', end='') # this prints no newline
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000635 self.print_change(change)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000636 print()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100637
638 # Changes referencing others' issues.
639 for issue_uid in ref_issues:
640 assert changes_by_ref_issue_uid[issue_uid]
641 self.print_issue(ref_issues[issue_uid])
642 for change in changes_by_ref_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000643 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100644 self.print_change(change)
645
646 # Changes referencing no issues.
647 if changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000648 print(self.options.output_format_no_url.format(title='Other changes'))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100649 for change in changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000650 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100651 self.print_change(change)
652
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000653 def print_activity(self):
654 self.print_changes()
655 self.print_reviews()
656 self.print_issues()
657
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000658 def dump_json(self, ignore_keys=None):
659 if ignore_keys is None:
660 ignore_keys = ['replies']
661
662 def format_for_json_dump(in_array):
663 output = {}
664 for item in in_array:
665 url = item.get('url') or item.get('review_url')
666 if not url:
667 raise Exception('Dumped item %s does not specify url' % item)
668 output[url] = dict(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000669 (k, v) for k,v in item.items() if k not in ignore_keys)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000670 return output
671
672 class PythonObjectEncoder(json.JSONEncoder):
673 def default(self, obj): # pylint: disable=method-hidden
674 if isinstance(obj, datetime):
675 return obj.isoformat()
676 if isinstance(obj, set):
677 return list(obj)
678 return json.JSONEncoder.default(self, obj)
679
680 output = {
681 'reviews': format_for_json_dump(self.reviews),
682 'changes': format_for_json_dump(self.changes),
683 'issues': format_for_json_dump(self.issues)
684 }
Raul Tambre80ee78e2019-05-06 22:41:05 +0000685 print(json.dumps(output, indent=2, cls=PythonObjectEncoder))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000686
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000687
688def main():
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000689 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
690 parser.add_option(
691 '-u', '--user', metavar='<email>',
Bruce Dawson84370962019-02-21 05:33:37 +0000692 # Look for USER and USERNAME (Windows) environment variables.
693 default=os.environ.get('USER', os.environ.get('USERNAME')),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000694 help='Filter on user, default=%default')
695 parser.add_option(
696 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000697 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000698 parser.add_option(
699 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000700 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000701 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
702 relativedelta(months=2))
703 parser.add_option(
704 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000705 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000706 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
707 parser.add_option(
708 '-Y', '--this_year', action='store_true',
709 help='Use this year\'s dates')
710 parser.add_option(
711 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000712 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000713 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000714 '-W', '--last_week', action='count',
715 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000716 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000717 '-a', '--auth',
718 action='store_true',
719 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000720 parser.add_option(
721 '-d', '--deltas',
722 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800723 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100724 parser.add_option(
725 '--no-referenced-issues',
726 action='store_true',
727 help='Do not fetch issues referenced by owned changes. Useful in '
728 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100729 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100730 parser.add_option(
Vadim Bendebury80012972019-11-23 02:23:11 +0000731 '--skip_servers',
732 action='store',
733 default='',
734 help='A comma separated list of gerrit and rietveld servers to ignore')
735 parser.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100736 '--skip-own-issues-without-changes',
737 action='store_true',
738 help='Skips listing own issues without changes when showing changes '
739 'grouped by referenced issue(s). See --changes-by-issue for more '
740 'details.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000741
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000742 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000743 'By default, all activity will be looked up and '
744 'printed. If any of these are specified, only '
745 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000746 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000747 '-c', '--changes',
748 action='store_true',
749 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000750 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000751 '-i', '--issues',
752 action='store_true',
753 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000754 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000755 '-r', '--reviews',
756 action='store_true',
757 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100758 activity_types_group.add_option(
759 '--changes-by-issue', action='store_true',
760 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000761 parser.add_option_group(activity_types_group)
762
763 output_format_group = optparse.OptionGroup(parser, 'Output Format',
764 'By default, all activity will be printed in the '
765 'following format: {url} {title}. This can be '
766 'changed for either all activity types or '
767 'individually for each activity type. The format '
768 'is defined as documented for '
769 'string.format(...). The variables available for '
Lutz Justen860378e2019-03-12 01:10:02 +0000770 'all activity types are url, title, author, '
771 'created and modified. Format options for '
772 'specific activity types will override the '
773 'generic format.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000774 output_format_group.add_option(
775 '-f', '--output-format', metavar='<format>',
776 default=u'{url} {title}',
777 help='Specifies the format to use when printing all your activity.')
778 output_format_group.add_option(
779 '--output-format-changes', metavar='<format>',
780 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000781 help='Specifies the format to use when printing changes. Supports the '
782 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000783 output_format_group.add_option(
784 '--output-format-issues', metavar='<format>',
785 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000786 help='Specifies the format to use when printing issues. Supports the '
787 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000788 output_format_group.add_option(
789 '--output-format-reviews', metavar='<format>',
790 default=None,
791 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000792 output_format_group.add_option(
793 '--output-format-heading', metavar='<format>',
794 default=u'{heading}:',
795 help='Specifies the format to use when printing headings.')
796 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100797 '--output-format-no-url', default='{title}',
798 help='Specifies the format to use when printing activity without url.')
799 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000800 '-m', '--markdown', action='store_true',
801 help='Use markdown-friendly output (overrides --output-format '
802 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000803 output_format_group.add_option(
804 '-j', '--json', action='store_true',
805 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000806 parser.add_option_group(output_format_group)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000807
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000808 parser.add_option(
809 '-v', '--verbose',
810 action='store_const',
811 dest='verbosity',
812 default=logging.WARN,
813 const=logging.INFO,
814 help='Output extra informational messages.'
815 )
816 parser.add_option(
817 '-q', '--quiet',
818 action='store_const',
819 dest='verbosity',
820 const=logging.ERROR,
821 help='Suppress non-error messages.'
822 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000823 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000824 '-M', '--merged-only',
825 action='store_true',
826 dest='merged_only',
827 default=False,
828 help='Shows only changes that have been merged.')
829 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000830 '-C', '--completed-issues',
831 action='store_true',
832 dest='completed_issues',
833 default=False,
834 help='Shows only monorail issues that have completed (Fixed|Verified) '
835 'by the user.')
836 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000837 '-o', '--output', metavar='<file>',
838 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000839
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000840 # Remove description formatting
841 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800842 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000843
844 options, args = parser.parse_args()
845 options.local_user = os.environ.get('USER')
846 if args:
847 parser.error('Args unsupported')
848 if not options.user:
Bruce Dawson84370962019-02-21 05:33:37 +0000849 parser.error('USER/USERNAME is not set, please use -u')
Peter K. Lee711dc5e2019-09-06 17:47:13 +0000850 # Retains the original -u option as the email address.
851 options.email = options.user
852 options.user = username(options.email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000853
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000854 logging.basicConfig(level=options.verbosity)
855
856 # python-keyring provides easy access to the system keyring.
857 try:
858 import keyring # pylint: disable=unused-import,unused-variable,F0401
859 except ImportError:
860 logging.warning('Consider installing python-keyring')
861
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000862 if not options.begin:
863 if options.last_quarter:
864 begin, end = quarter_begin, quarter_end
865 elif options.this_year:
866 begin, end = get_year_of(datetime.today())
867 elif options.week_of:
868 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000869 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000870 begin, end = (get_week_of(datetime.today() -
871 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000872 else:
873 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
874 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700875 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000876 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700877 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000878 else:
879 end = datetime.today()
880 options.begin, options.end = begin, end
Bruce Dawson2afcf222019-02-25 22:07:08 +0000881 if begin >= end:
882 # The queries fail in peculiar ways when the begin date is in the future.
883 # Give a descriptive error message instead.
884 logging.error('Start date (%s) is the same or later than end date (%s)' %
885 (begin, end))
886 return 1
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000887
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000888 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000889 options.output_format_heading = '### {heading}\n'
890 options.output_format = ' * [{title}]({url})'
891 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000892 logging.info('Searching for activity by %s', options.user)
893 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000894
895 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100896 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000897
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100898 if not (options.changes or options.reviews or options.issues or
899 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000900 options.changes = True
901 options.issues = True
902 options.reviews = True
903
904 # First do any required authentication so none of the user interaction has to
905 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100906 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000907 my_activity.auth_for_changes()
908 if options.reviews:
909 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000910
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000911 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000912
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000913 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100914 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000915 my_activity.get_changes()
916 if options.reviews:
917 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100918 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000919 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100920 if not options.no_referenced_issues:
921 my_activity.get_referenced_issues()
Edward Lemur5b929a42019-10-21 17:57:39 +0000922 except auth.LoginRequiredError as e:
923 logging.error('auth.LoginRequiredError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000924
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100925 my_activity.show_progress('\n')
926
Vadim Bendebury8de38002018-05-14 19:02:55 -0700927 my_activity.print_access_errors()
928
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000929 output_file = None
930 try:
931 if options.output:
932 output_file = open(options.output, 'w')
933 logging.info('Printing output to "%s"', options.output)
934 sys.stdout = output_file
935 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700936 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000937 else:
938 if options.json:
939 my_activity.dump_json()
940 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100941 if options.changes:
942 my_activity.print_changes()
943 if options.reviews:
944 my_activity.print_reviews()
945 if options.issues:
946 my_activity.print_issues()
947 if options.changes_by_issue:
948 my_activity.print_changes_by_issue(
949 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000950 finally:
951 if output_file:
952 logging.info('Done printing to file.')
953 sys.stdout = sys.__stdout__
954 output_file.close()
955
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000956 return 0
957
958
959if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +0000960 # Fix encoding to support non-ascii issue titles.
961 fix_encoding.fix_encoding()
962
sbc@chromium.org013731e2015-02-26 18:28:43 +0000963 try:
964 sys.exit(main())
965 except KeyboardInterrupt:
966 sys.stderr.write('interrupted\n')
967 sys.exit(1)