blob: 1e3fbcd6bb4defe0e916284196c398485b0e45b1 [file] [log] [blame]
Gabriel Charettebc6617a2019-02-05 21:30:52 +00001#!/usr/bin/env vpython
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Get stats about your activity.
7
8Example:
9 - my_activity.py for stats for the current week (last week on mondays).
10 - my_activity.py -Q for stats for last quarter.
11 - my_activity.py -Y for stats for this year.
Mohamed Heikaldc37feb2019-06-28 22:33:58 +000012 - my_activity.py -b 4/24/19 for stats since April 24th 2019.
13 - my_activity.py -b 4/24/19 -e 6/16/19 stats between April 24th and June 16th.
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +000014 - my_activity.py -jd to output stats for the week to json with deltas data.
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000015"""
16
17# These services typically only provide a created time and a last modified time
18# for each item for general queries. This is not enough to determine if there
19# was activity in a given time period. So, we first query for all things created
20# before end and modified after begin. Then, we get the details of each item and
21# check those details to determine if there was activity in the given period.
22# This means that query time scales mostly with (today() - begin).
23
Gabriel Charettebc6617a2019-02-05 21:30:52 +000024# [VPYTHON:BEGIN]
25# wheel: <
26# name: "infra/python/wheels/python-dateutil-py2_py3"
27# version: "version:2.7.3"
28# >
29# wheel: <
30# name: "infra/python/wheels/six-py2_py3"
31# version: "version:1.10.0"
32# >
33# [VPYTHON:END]
34
Raul Tambre80ee78e2019-05-06 22:41:05 +000035from __future__ import print_function
36
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010037import collections
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +010038import contextlib
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000039from datetime import datetime
40from datetime import timedelta
41from functools import partial
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010042import itertools
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000043import json
Tobias Sargeantffb3c432017-03-08 14:09:14 +000044import logging
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +010045from multiprocessing.pool import ThreadPool
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000046import optparse
47import os
48import subprocess
Tobias Sargeantffb3c432017-03-08 14:09:14 +000049from string import Formatter
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000050import sys
51import urllib
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +000052import re
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000053
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000054import auth
stevefung@chromium.org832d51e2015-05-27 12:52:51 +000055import fix_encoding
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000056import gerrit_util
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000057
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +000058from third_party import httplib2
59
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000060try:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000061 import dateutil # pylint: disable=import-error
62 import dateutil.parser
63 from dateutil.relativedelta import relativedelta
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000064except ImportError:
Tobias Sargeantffb3c432017-03-08 14:09:14 +000065 logging.error('python-dateutil package required')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000066 exit(1)
67
Tobias Sargeantffb3c432017-03-08 14:09:14 +000068
69class DefaultFormatter(Formatter):
70 def __init__(self, default = ''):
71 super(DefaultFormatter, self).__init__()
72 self.default = default
73
74 def get_value(self, key, args, kwds):
75 if isinstance(key, basestring) and key not in kwds:
76 return self.default
77 return Formatter.get_value(self, key, args, kwds)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000078
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000079gerrit_instances = [
80 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000081 'url': 'android-review.googlesource.com',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000082 },
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000083 {
84 'url': 'chrome-internal-review.googlesource.com',
Jeremy Romanf475a472017-06-06 16:49:11 -040085 'shorturl': 'crrev.com/i',
Varun Khanejad9f97bc2017-08-02 12:55:01 -070086 'short_url_protocol': 'https',
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000087 },
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000088 {
Adrienne Walker95d4c852018-09-27 20:28:12 +000089 'url': 'chromium-review.googlesource.com',
90 'shorturl': 'crrev.com/c',
91 'short_url_protocol': 'https',
deymo@chromium.org56dc57a2015-09-10 18:26:54 +000092 },
Ryan Harrison897602a2017-09-18 16:23:41 -040093 {
94 'url': 'pdfium-review.googlesource.com',
95 },
Adrienne Walker95d4c852018-09-27 20:28:12 +000096 {
97 'url': 'skia-review.googlesource.com',
98 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000099]
100
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100101monorail_projects = {
102 'chromium': {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000103 'shorturl': 'crbug.com',
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700104 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000105 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100106 'google-breakpad': {},
107 'gyp': {},
108 'skia': {},
109 'pdfium': {
Ryan Harrison897602a2017-09-18 16:23:41 -0400110 'shorturl': 'crbug.com/pdfium',
111 'short_url_protocol': 'https',
112 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100113 'v8': {
114 'shorturl': 'crbug.com/v8',
115 'short_url_protocol': 'https',
116 },
117}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000118
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000119def username(email):
120 """Keeps the username of an email address."""
121 return email and email.split('@', 1)[0]
122
123
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000124def datetime_to_midnight(date):
125 return date - timedelta(hours=date.hour, minutes=date.minute,
126 seconds=date.second, microseconds=date.microsecond)
127
128
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000129def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000130 begin = (datetime_to_midnight(date) -
131 relativedelta(months=(date.month % 3) - 1, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000132 return begin, begin + relativedelta(months=3)
133
134
135def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000136 begin = (datetime_to_midnight(date) -
137 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000138 return begin, begin + relativedelta(years=1)
139
140
141def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000142 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000143 return begin, begin + timedelta(days=7)
144
145
146def get_yes_or_no(msg):
147 while True:
148 response = raw_input(msg + ' yes/no [no] ')
149 if response == 'y' or response == 'yes':
150 return True
151 elif not response or response == 'n' or response == 'no':
152 return False
153
154
deymo@chromium.org6c039202013-09-12 12:28:12 +0000155def datetime_from_gerrit(date_string):
156 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
157
158
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100159def datetime_from_monorail(date_string):
160 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000161
162
163class MyActivity(object):
164 def __init__(self, options):
165 self.options = options
166 self.modified_after = options.begin
167 self.modified_before = options.end
168 self.user = options.user
169 self.changes = []
170 self.reviews = []
171 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100172 self.referenced_issues = []
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000173 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-14 19:02:55 -0700174 self.access_errors = set()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000175
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100176 def show_progress(self, how='.'):
177 if sys.stdout.isatty():
178 sys.stdout.write(how)
179 sys.stdout.flush()
180
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000181 def extract_bug_numbers_from_description(self, issue):
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000182 description = None
183
184 if 'description' in issue:
185 # Getting the description for Rietveld
186 description = issue['description']
187 elif 'revisions' in issue:
188 # Getting the description for REST Gerrit
189 revision = issue['revisions'][issue['current_revision']]
190 description = revision['commit']['message']
191
192 bugs = []
193 if description:
Nicolas Dossou-gbete903ea732017-07-10 16:46:59 +0100194 # Handle both "Bug: 99999" and "BUG=99999" bug notations
195 # Multiple bugs can be noted on a single line or in multiple ones.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100196 matches = re.findall(
197 r'BUG[=:]\s?((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)', description,
198 flags=re.IGNORECASE)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000199 if matches:
200 for match in matches:
201 bugs.extend(match[0].replace(' ', '').split(','))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100202 # Add default chromium: prefix if none specified.
203 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000204
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000205 return sorted(set(bugs))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000206
Vadim Bendebury8de38002018-05-14 19:02:55 -0700207 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200208 # Convert the "key:value" filter to a list of (key, value) pairs.
209 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000210 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000211 # Instantiate the generator to force all the requests now and catch the
212 # errors here.
213 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000214 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
215 'CURRENT_REVISION', 'CURRENT_COMMIT']))
Raul Tambre7c938462019-05-24 16:35:35 +0000216 except gerrit_util.GerritError as e:
Vadim Bendebury8de38002018-05-14 19:02:55 -0700217 error_message = 'Looking up %r: %s' % (instance['url'], e)
218 if error_message not in self.access_errors:
219 self.access_errors.add(error_message)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000220 return []
221
deymo@chromium.org6c039202013-09-12 12:28:12 +0000222 def gerrit_search(self, instance, owner=None, reviewer=None):
223 max_age = datetime.today() - self.modified_after
Andrii Shyshkalov4aedec02018-09-17 16:31:17 +0000224 filters = ['-age:%ss' % (max_age.days * 24 * 3600 + max_age.seconds)]
225 if owner:
226 assert not reviewer
227 filters.append('owner:%s' % owner)
228 else:
229 filters.extend(('-owner:%s' % reviewer, 'reviewer:%s' % reviewer))
Peter K. Lee9342ac02018-08-28 21:03:51 +0000230 # TODO(cjhopman): Should abandoned changes be filtered out when
231 # merged_only is not enabled?
232 if self.options.merged_only:
233 filters.append('status:merged')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000234
Aaron Gable2979a872017-09-05 17:38:32 -0700235 issues = self.gerrit_changes_over_rest(instance, filters)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100236 self.show_progress()
Aaron Gable2979a872017-09-05 17:38:32 -0700237 issues = [self.process_gerrit_issue(instance, issue)
238 for issue in issues]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000239
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000240 issues = filter(self.filter_issue, issues)
241 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
242
243 return issues
244
Aaron Gable2979a872017-09-05 17:38:32 -0700245 def process_gerrit_issue(self, instance, issue):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000246 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000247 if self.options.deltas:
248 ret['delta'] = DefaultFormatter().format(
249 '+{insertions},-{deletions}',
250 **issue)
251 ret['status'] = issue['status']
deymo@chromium.org6c039202013-09-12 12:28:12 +0000252 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700253 protocol = instance.get('short_url_protocol', 'http')
254 url = instance['shorturl']
255 else:
256 protocol = 'https'
257 url = instance['url']
258 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
259
deymo@chromium.org6c039202013-09-12 12:28:12 +0000260 ret['header'] = issue['subject']
Don Garrett2ebf9fd2018-08-06 15:14:00 +0000261 ret['owner'] = issue['owner'].get('email', '')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000262 ret['author'] = ret['owner']
263 ret['created'] = datetime_from_gerrit(issue['created'])
264 ret['modified'] = datetime_from_gerrit(issue['updated'])
265 if 'messages' in issue:
Aaron Gable2979a872017-09-05 17:38:32 -0700266 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
deymo@chromium.org6c039202013-09-12 12:28:12 +0000267 else:
268 ret['replies'] = []
269 ret['reviewers'] = set(r['author'] for r in ret['replies'])
270 ret['reviewers'].discard(ret['author'])
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000271 ret['bugs'] = self.extract_bug_numbers_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000272 return ret
273
274 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700275 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000276 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000277 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
278 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000279 for reply in replies:
280 ret.append({
281 'author': reply['author']['email'],
282 'created': datetime_from_gerrit(reply['date']),
283 'content': reply['message'],
284 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000285 return ret
286
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100287 def monorail_get_auth_http(self):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000288 auth_config = auth.extract_auth_config_from_options(self.options)
289 authenticator = auth.get_authenticator_for_host(
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000290 'bugs.chromium.org', auth_config)
Kenneth Russell66badbd2018-09-09 21:35:32 +0000291 # Manually use a long timeout (10m); for some users who have a
292 # long history on the issue tracker, whatever the default timeout
293 # is is reached.
294 return authenticator.authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100295
296 def filter_modified_monorail_issue(self, issue):
297 """Precisely checks if an issue has been modified in the time range.
298
299 This fetches all issue comments to check if the issue has been modified in
300 the time range specified by user. This is needed because monorail only
301 allows filtering by last updated and published dates, which is not
302 sufficient to tell whether a given issue has been modified at some specific
303 time range. Any update to the issue is a reported as comment on Monorail.
304
305 Args:
306 issue: Issue dict as returned by monorail_query_issues method. In
307 particular, must have a key 'uid' formatted as 'project:issue_id'.
308
309 Returns:
310 Passed issue if modified, None otherwise.
311 """
312 http = self.monorail_get_auth_http()
313 project, issue_id = issue['uid'].split(':')
314 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
315 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
316 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100317 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100318 content = json.loads(body)
319 if not content:
320 logging.error('Unable to parse %s response from monorail.', project)
321 return issue
322
323 for item in content.get('items', []):
324 comment_published = datetime_from_monorail(item['published'])
325 if self.filter_modified(comment_published):
326 return issue
327
328 return None
329
330 def monorail_query_issues(self, project, query):
331 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000332 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100333 '/%s/issues') % project
334 query_data = urllib.urlencode(query)
335 url = url + '?' + query_data
336 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100337 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100338 content = json.loads(body)
339 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100340 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100341 return []
342
343 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100344 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100345 for item in content.get('items', []):
346 if project_config.get('shorturl'):
347 protocol = project_config.get('short_url_protocol', 'http')
348 item_url = '%s://%s/%d' % (
349 protocol, project_config['shorturl'], item['id'])
350 else:
351 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
352 project, item['id'])
353 issue = {
354 'uid': '%s:%s' % (project, item['id']),
355 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100356 'created': datetime_from_monorail(item['published']),
357 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100358 'author': item['author']['name'],
359 'url': item_url,
360 'comments': [],
361 'status': item['status'],
362 'labels': [],
363 'components': []
364 }
365 if 'owner' in item:
366 issue['owner'] = item['owner']['name']
367 else:
368 issue['owner'] = 'None'
369 if 'labels' in item:
370 issue['labels'] = item['labels']
371 if 'components' in item:
372 issue['components'] = item['components']
373 issues.append(issue)
374
375 return issues
376
377 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000378 epoch = datetime.utcfromtimestamp(0)
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000379 # TODO(tandrii): support non-chromium email, too.
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000380 user_str = '%s@chromium.org' % self.user
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000381
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100382 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000383 'maxResults': 10000,
384 'q': user_str,
385 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
386 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000387 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000388
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000389 if self.options.completed_issues:
390 return [
391 issue for issue in issues
392 if (self.match(issue['owner']) and
393 issue['status'].lower() in ('verified', 'fixed'))
394 ]
395
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100396 return [
397 issue for issue in issues
398 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000399
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100400 def monorail_get_issues(self, project, issue_ids):
401 return self.monorail_query_issues(project, {
402 'maxResults': 10000,
403 'q': 'id:%s' % ','.join(issue_ids)
404 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000405
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000406 def print_heading(self, heading):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000407 print()
408 print(self.options.output_format_heading.format(heading=heading))
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000409
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000410 def match(self, author):
411 if '@' in self.user:
412 return author == self.user
413 return author.startswith(self.user + '@')
414
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000415 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000416 activity = len([
417 reply
418 for reply in change['replies']
419 if self.match(reply['author'])
420 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000421 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000422 'created': change['created'].date().isoformat(),
423 'modified': change['modified'].date().isoformat(),
424 'reviewers': ', '.join(change['reviewers']),
425 'status': change['status'],
426 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000427 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000428 if self.options.deltas:
429 optional_values['delta'] = change['delta']
430
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000431 self.print_generic(self.options.output_format,
432 self.options.output_format_changes,
433 change['header'],
434 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000435 change['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000436 change['created'],
437 change['modified'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000438 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000439
440 def print_issue(self, issue):
441 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000442 'created': issue['created'].date().isoformat(),
443 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000444 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000445 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000446 }
447 self.print_generic(self.options.output_format,
448 self.options.output_format_issues,
449 issue['header'],
450 issue['url'],
451 issue['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000452 issue['created'],
453 issue['modified'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000454 optional_values)
455
456 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000457 activity = len([
458 reply
459 for reply in review['replies']
460 if self.match(reply['author'])
461 ])
462 optional_values = {
463 'created': review['created'].date().isoformat(),
464 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800465 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000466 'activity': activity,
467 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800468 if self.options.deltas:
469 optional_values['delta'] = review['delta']
470
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000471 self.print_generic(self.options.output_format,
472 self.options.output_format_reviews,
473 review['header'],
474 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000475 review['author'],
Lutz Justen860378e2019-03-12 01:10:02 +0000476 review['created'],
477 review['modified'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000478 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000479
480 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000481 def print_generic(default_fmt, specific_fmt,
Lutz Justen860378e2019-03-12 01:10:02 +0000482 title, url, author, created, modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000483 optional_values=None):
484 output_format = specific_fmt if specific_fmt is not None else default_fmt
485 output_format = unicode(output_format)
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000486 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000487 'title': title,
488 'url': url,
489 'author': author,
Lutz Justen860378e2019-03-12 01:10:02 +0000490 'created': created,
491 'modified': modified,
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000492 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000493 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000494 values.update(optional_values)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000495 print(DefaultFormatter().format(output_format,
496 **values).encode(sys.getdefaultencoding()))
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000497
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000498
499 def filter_issue(self, issue, should_filter_by_user=True):
500 def maybe_filter_username(email):
501 return not should_filter_by_user or username(email) == self.user
502 if (maybe_filter_username(issue['author']) and
503 self.filter_modified(issue['created'])):
504 return True
505 if (maybe_filter_username(issue['owner']) and
506 (self.filter_modified(issue['created']) or
507 self.filter_modified(issue['modified']))):
508 return True
509 for reply in issue['replies']:
510 if self.filter_modified(reply['created']):
511 if not should_filter_by_user:
512 break
513 if (username(reply['author']) == self.user
514 or (self.user + '@') in reply['content']):
515 break
516 else:
517 return False
518 return True
519
520 def filter_modified(self, modified):
521 return self.modified_after < modified and modified < self.modified_before
522
523 def auth_for_changes(self):
524 #TODO(cjhopman): Move authentication check for getting changes here.
525 pass
526
527 def auth_for_reviews(self):
528 # Reviews use all the same instances as changes so no authentication is
529 # required.
530 pass
531
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000532 def get_changes(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000533 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100534 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100535 gerrit_changes = pool.map_async(
536 lambda instance: self.gerrit_search(instance, owner=self.user),
537 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100538 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000539 self.changes = list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000540
541 def print_changes(self):
542 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000543 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000544 for change in self.changes:
Bruce Dawson2afcf222019-02-25 22:07:08 +0000545 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000546
Vadim Bendebury8de38002018-05-14 19:02:55 -0700547 def print_access_errors(self):
548 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400549 logging.error('Access Errors:')
550 for error in self.access_errors:
551 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700552
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000553 def get_reviews(self):
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000554 num_instances = len(gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100555 with contextlib.closing(ThreadPool(num_instances)) as pool:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100556 gerrit_reviews = pool.map_async(
557 lambda instance: self.gerrit_search(instance, reviewer=self.user),
558 gerrit_instances)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100559 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Edward Lemurf4e0cc62019-07-18 23:44:23 +0000560 self.reviews = list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000561
562 def print_reviews(self):
563 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000564 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000565 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000566 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000567
568 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100569 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
570 monorail_issues = pool.map(
571 self.monorail_issue_search, monorail_projects.keys())
572 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
573
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700574 if not monorail_issues:
575 return
576
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100577 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
578 filtered_issues = pool.map(
579 self.filter_modified_monorail_issue, monorail_issues)
580 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100581
582 def get_referenced_issues(self):
583 if not self.issues:
584 self.get_issues()
585
586 if not self.changes:
587 self.get_changes()
588
589 referenced_issue_uids = set(itertools.chain.from_iterable(
590 change['bugs'] for change in self.changes))
591 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
592 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
593
594 missing_issues_by_project = collections.defaultdict(list)
595 for issue_uid in missing_issue_uids:
596 project, issue_id = issue_uid.split(':')
597 missing_issues_by_project[project].append(issue_id)
598
599 for project, issue_ids in missing_issues_by_project.iteritems():
600 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000601
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000602 def print_issues(self):
603 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000604 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000605 for issue in self.issues:
606 self.print_issue(issue)
607
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100608 def print_changes_by_issue(self, skip_empty_own):
609 if not self.issues or not self.changes:
610 return
611
612 self.print_heading('Changes by referenced issue(s)')
613 issues = {issue['uid']: issue for issue in self.issues}
614 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
615 changes_by_issue_uid = collections.defaultdict(list)
616 changes_by_ref_issue_uid = collections.defaultdict(list)
617 changes_without_issue = []
618 for change in self.changes:
619 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000620 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100621 if issue_uid in issues:
622 changes_by_issue_uid[issue_uid].append(change)
623 added = True
624 if issue_uid in ref_issues:
625 changes_by_ref_issue_uid[issue_uid].append(change)
626 added = True
627 if not added:
628 changes_without_issue.append(change)
629
630 # Changes referencing own issues.
631 for issue_uid in issues:
632 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
633 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000634 if changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000635 print()
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000636 for change in changes_by_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000637 print(' ', end='') # this prints no newline
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000638 self.print_change(change)
Raul Tambre80ee78e2019-05-06 22:41:05 +0000639 print()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100640
641 # Changes referencing others' issues.
642 for issue_uid in ref_issues:
643 assert changes_by_ref_issue_uid[issue_uid]
644 self.print_issue(ref_issues[issue_uid])
645 for change in changes_by_ref_issue_uid[issue_uid]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000646 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100647 self.print_change(change)
648
649 # Changes referencing no issues.
650 if changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000651 print(self.options.output_format_no_url.format(title='Other changes'))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100652 for change in changes_without_issue:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000653 print('', end=' ') # this prints one space due to comma, but no newline
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100654 self.print_change(change)
655
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000656 def print_activity(self):
657 self.print_changes()
658 self.print_reviews()
659 self.print_issues()
660
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000661 def dump_json(self, ignore_keys=None):
662 if ignore_keys is None:
663 ignore_keys = ['replies']
664
665 def format_for_json_dump(in_array):
666 output = {}
667 for item in in_array:
668 url = item.get('url') or item.get('review_url')
669 if not url:
670 raise Exception('Dumped item %s does not specify url' % item)
671 output[url] = dict(
672 (k, v) for k,v in item.iteritems() if k not in ignore_keys)
673 return output
674
675 class PythonObjectEncoder(json.JSONEncoder):
676 def default(self, obj): # pylint: disable=method-hidden
677 if isinstance(obj, datetime):
678 return obj.isoformat()
679 if isinstance(obj, set):
680 return list(obj)
681 return json.JSONEncoder.default(self, obj)
682
683 output = {
684 'reviews': format_for_json_dump(self.reviews),
685 'changes': format_for_json_dump(self.changes),
686 'issues': format_for_json_dump(self.issues)
687 }
Raul Tambre80ee78e2019-05-06 22:41:05 +0000688 print(json.dumps(output, indent=2, cls=PythonObjectEncoder))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000689
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000690
691def main():
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000692 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
693 parser.add_option(
694 '-u', '--user', metavar='<email>',
Bruce Dawson84370962019-02-21 05:33:37 +0000695 # Look for USER and USERNAME (Windows) environment variables.
696 default=os.environ.get('USER', os.environ.get('USERNAME')),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000697 help='Filter on user, default=%default')
698 parser.add_option(
699 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000700 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000701 parser.add_option(
702 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000703 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000704 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
705 relativedelta(months=2))
706 parser.add_option(
707 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000708 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000709 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
710 parser.add_option(
711 '-Y', '--this_year', action='store_true',
712 help='Use this year\'s dates')
713 parser.add_option(
714 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000715 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000716 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000717 '-W', '--last_week', action='count',
718 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000719 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000720 '-a', '--auth',
721 action='store_true',
722 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000723 parser.add_option(
724 '-d', '--deltas',
725 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800726 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100727 parser.add_option(
728 '--no-referenced-issues',
729 action='store_true',
730 help='Do not fetch issues referenced by owned changes. Useful in '
731 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100732 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100733 parser.add_option(
734 '--skip-own-issues-without-changes',
735 action='store_true',
736 help='Skips listing own issues without changes when showing changes '
737 'grouped by referenced issue(s). See --changes-by-issue for more '
738 'details.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000739
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000740 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000741 'By default, all activity will be looked up and '
742 'printed. If any of these are specified, only '
743 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000744 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000745 '-c', '--changes',
746 action='store_true',
747 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000748 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000749 '-i', '--issues',
750 action='store_true',
751 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000752 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000753 '-r', '--reviews',
754 action='store_true',
755 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100756 activity_types_group.add_option(
757 '--changes-by-issue', action='store_true',
758 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000759 parser.add_option_group(activity_types_group)
760
761 output_format_group = optparse.OptionGroup(parser, 'Output Format',
762 'By default, all activity will be printed in the '
763 'following format: {url} {title}. This can be '
764 'changed for either all activity types or '
765 'individually for each activity type. The format '
766 'is defined as documented for '
767 'string.format(...). The variables available for '
Lutz Justen860378e2019-03-12 01:10:02 +0000768 'all activity types are url, title, author, '
769 'created and modified. Format options for '
770 'specific activity types will override the '
771 'generic format.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000772 output_format_group.add_option(
773 '-f', '--output-format', metavar='<format>',
774 default=u'{url} {title}',
775 help='Specifies the format to use when printing all your activity.')
776 output_format_group.add_option(
777 '--output-format-changes', metavar='<format>',
778 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000779 help='Specifies the format to use when printing changes. Supports the '
780 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000781 output_format_group.add_option(
782 '--output-format-issues', metavar='<format>',
783 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000784 help='Specifies the format to use when printing issues. Supports the '
785 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000786 output_format_group.add_option(
787 '--output-format-reviews', metavar='<format>',
788 default=None,
789 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000790 output_format_group.add_option(
791 '--output-format-heading', metavar='<format>',
792 default=u'{heading}:',
793 help='Specifies the format to use when printing headings.')
794 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100795 '--output-format-no-url', default='{title}',
796 help='Specifies the format to use when printing activity without url.')
797 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000798 '-m', '--markdown', action='store_true',
799 help='Use markdown-friendly output (overrides --output-format '
800 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000801 output_format_group.add_option(
802 '-j', '--json', action='store_true',
803 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000804 parser.add_option_group(output_format_group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000805 auth.add_auth_options(parser)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000806
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000807 parser.add_option(
808 '-v', '--verbose',
809 action='store_const',
810 dest='verbosity',
811 default=logging.WARN,
812 const=logging.INFO,
813 help='Output extra informational messages.'
814 )
815 parser.add_option(
816 '-q', '--quiet',
817 action='store_const',
818 dest='verbosity',
819 const=logging.ERROR,
820 help='Suppress non-error messages.'
821 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000822 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000823 '-M', '--merged-only',
824 action='store_true',
825 dest='merged_only',
826 default=False,
827 help='Shows only changes that have been merged.')
828 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000829 '-C', '--completed-issues',
830 action='store_true',
831 dest='completed_issues',
832 default=False,
833 help='Shows only monorail issues that have completed (Fixed|Verified) '
834 'by the user.')
835 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000836 '-o', '--output', metavar='<file>',
837 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000838
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000839 # Remove description formatting
840 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800841 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000842
843 options, args = parser.parse_args()
844 options.local_user = os.environ.get('USER')
845 if args:
846 parser.error('Args unsupported')
847 if not options.user:
Bruce Dawson84370962019-02-21 05:33:37 +0000848 parser.error('USER/USERNAME is not set, please use -u')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000849 options.user = username(options.user)
850
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()
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000919 except auth.AuthenticationError as e:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000920 logging.error('auth.AuthenticationError: %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)