blob: 3eebbb5a8e68bb76d5c08234021815a81cab045b [file] [log] [blame]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001#!/usr/bin/env python
2# 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.
12 - my_activity.py -b 4/5/12 for stats since 4/5/12.
13 - my_activity.py -b 4/5/12 -e 6/7/12 for stats between 4/5/12 and 6/7/12.
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
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010024import collections
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +010025import contextlib
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000026from datetime import datetime
27from datetime import timedelta
28from functools import partial
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +010029import itertools
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000030import json
Tobias Sargeantffb3c432017-03-08 14:09:14 +000031import logging
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +010032from multiprocessing.pool import ThreadPool
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000033import optparse
34import os
35import subprocess
Tobias Sargeantffb3c432017-03-08 14:09:14 +000036from string import Formatter
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000037import sys
38import urllib
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +000039import re
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000040
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000041import auth
stevefung@chromium.org832d51e2015-05-27 12:52:51 +000042import fix_encoding
deymo@chromium.orgf8be2762013-11-06 01:01:59 +000043import gerrit_util
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000044import rietveld
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000045
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +000046from third_party import httplib2
47
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 +000067rietveld_instances = [
68 {
69 'url': 'codereview.chromium.org',
70 'shorturl': 'crrev.com',
71 'supports_owner_modified_query': True,
72 'requires_auth': False,
73 'email_domain': 'chromium.org',
Varun Khanejad9f97bc2017-08-02 12:55:01 -070074 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +000075 },
76 {
77 'url': 'chromereviews.googleplex.com',
78 'shorturl': 'go/chromerev',
79 'supports_owner_modified_query': True,
80 'requires_auth': True,
81 'email_domain': 'google.com',
82 },
83 {
84 'url': 'codereview.appspot.com',
85 'supports_owner_modified_query': True,
86 'requires_auth': False,
87 'email_domain': 'chromium.org',
88 },
89 {
90 'url': 'breakpad.appspot.com',
91 'supports_owner_modified_query': False,
92 'requires_auth': False,
93 'email_domain': 'chromium.org',
94 },
95]
96
97gerrit_instances = [
98 {
deymo@chromium.org6c039202013-09-12 12:28:12 +000099 'url': 'chromium-review.googlesource.com',
Jeremy Romanf475a472017-06-06 16:49:11 -0400100 'shorturl': 'crrev.com/c',
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700101 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000102 },
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000103 {
104 'url': 'chrome-internal-review.googlesource.com',
Jeremy Romanf475a472017-06-06 16:49:11 -0400105 'shorturl': 'crrev.com/i',
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700106 'short_url_protocol': 'https',
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000107 },
deymo@chromium.org56dc57a2015-09-10 18:26:54 +0000108 {
109 'url': 'android-review.googlesource.com',
110 },
Ryan Harrison897602a2017-09-18 16:23:41 -0400111 {
Raul E Rangelc27134c2018-08-20 19:15:09 +0000112 'url': 'review.coreboot.org',
113 },
114 {
Ryan Harrison897602a2017-09-18 16:23:41 -0400115 'url': 'pdfium-review.googlesource.com',
116 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000117]
118
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100119monorail_projects = {
120 'chromium': {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000121 'shorturl': 'crbug.com',
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700122 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000123 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100124 'google-breakpad': {},
125 'gyp': {},
126 'skia': {},
127 'pdfium': {
Ryan Harrison897602a2017-09-18 16:23:41 -0400128 'shorturl': 'crbug.com/pdfium',
129 'short_url_protocol': 'https',
130 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100131 'v8': {
132 'shorturl': 'crbug.com/v8',
133 'short_url_protocol': 'https',
134 },
135}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000136
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000137def username(email):
138 """Keeps the username of an email address."""
139 return email and email.split('@', 1)[0]
140
141
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000142def datetime_to_midnight(date):
143 return date - timedelta(hours=date.hour, minutes=date.minute,
144 seconds=date.second, microseconds=date.microsecond)
145
146
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000147def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000148 begin = (datetime_to_midnight(date) -
149 relativedelta(months=(date.month % 3) - 1, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000150 return begin, begin + relativedelta(months=3)
151
152
153def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000154 begin = (datetime_to_midnight(date) -
155 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000156 return begin, begin + relativedelta(years=1)
157
158
159def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000160 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000161 return begin, begin + timedelta(days=7)
162
163
164def get_yes_or_no(msg):
165 while True:
166 response = raw_input(msg + ' yes/no [no] ')
167 if response == 'y' or response == 'yes':
168 return True
169 elif not response or response == 'n' or response == 'no':
170 return False
171
172
deymo@chromium.org6c039202013-09-12 12:28:12 +0000173def datetime_from_gerrit(date_string):
174 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
175
176
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000177def datetime_from_rietveld(date_string):
deymo@chromium.org29eb6e62014-03-20 01:55:55 +0000178 try:
179 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f')
180 except ValueError:
181 # Sometimes rietveld returns a value without the milliseconds part, so we
182 # attempt to parse those cases as well.
183 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000184
185
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100186def datetime_from_monorail(date_string):
187 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000188
189
190class MyActivity(object):
191 def __init__(self, options):
192 self.options = options
193 self.modified_after = options.begin
194 self.modified_before = options.end
195 self.user = options.user
196 self.changes = []
197 self.reviews = []
198 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100199 self.referenced_issues = []
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000200 self.check_cookies()
201 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-14 19:02:55 -0700202 self.access_errors = set()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000203
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100204 def show_progress(self, how='.'):
205 if sys.stdout.isatty():
206 sys.stdout.write(how)
207 sys.stdout.flush()
208
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000209 # Check the codereview cookie jar to determine which Rietveld instances to
210 # authenticate to.
211 def check_cookies(self):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000212 filtered_instances = []
213
214 def has_cookie(instance):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000215 auth_config = auth.extract_auth_config_from_options(self.options)
216 a = auth.get_authenticator_for_host(instance['url'], auth_config)
217 return a.has_cached_credentials()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000218
219 for instance in rietveld_instances:
220 instance['auth'] = has_cookie(instance)
221
222 if filtered_instances:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000223 logging.warning('No cookie found for the following Rietveld instance%s:',
224 's' if len(filtered_instances) > 1 else '')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000225 for instance in filtered_instances:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000226 logging.warning('\t' + instance['url'])
227 logging.warning('Use --auth if you would like to authenticate to them.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000228
229 def rietveld_search(self, instance, owner=None, reviewer=None):
230 if instance['requires_auth'] and not instance['auth']:
231 return []
232
233
234 email = None if instance['auth'] else ''
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000235 auth_config = auth.extract_auth_config_from_options(self.options)
236 remote = rietveld.Rietveld('https://' + instance['url'], auth_config, email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000237
238 # See def search() in rietveld.py to see all the filters you can use.
239 query_modified_after = None
240
241 if instance['supports_owner_modified_query']:
242 query_modified_after = self.modified_after.strftime('%Y-%m-%d')
243
244 # Rietveld does not allow search by both created_before and modified_after.
245 # (And some instances don't allow search by both owner and modified_after)
246 owner_email = None
247 reviewer_email = None
248 if owner:
249 owner_email = owner + '@' + instance['email_domain']
250 if reviewer:
251 reviewer_email = reviewer + '@' + instance['email_domain']
252 issues = remote.search(
253 owner=owner_email,
254 reviewer=reviewer_email,
255 modified_after=query_modified_after,
256 with_messages=True)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100257 self.show_progress()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000258
259 issues = filter(
260 lambda i: (datetime_from_rietveld(i['created']) < self.modified_before),
261 issues)
262 issues = filter(
263 lambda i: (datetime_from_rietveld(i['modified']) > self.modified_after),
264 issues)
265
266 should_filter_by_user = True
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000267 issues = map(partial(self.process_rietveld_issue, remote, instance), issues)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000268 issues = filter(
269 partial(self.filter_issue, should_filter_by_user=should_filter_by_user),
270 issues)
271 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
272
273 return issues
274
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000275 def extract_bug_numbers_from_description(self, issue):
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000276 description = None
277
278 if 'description' in issue:
279 # Getting the description for Rietveld
280 description = issue['description']
281 elif 'revisions' in issue:
282 # Getting the description for REST Gerrit
283 revision = issue['revisions'][issue['current_revision']]
284 description = revision['commit']['message']
285
286 bugs = []
287 if description:
Nicolas Dossou-gbete903ea732017-07-10 16:46:59 +0100288 # Handle both "Bug: 99999" and "BUG=99999" bug notations
289 # Multiple bugs can be noted on a single line or in multiple ones.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100290 matches = re.findall(
291 r'BUG[=:]\s?((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)', description,
292 flags=re.IGNORECASE)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000293 if matches:
294 for match in matches:
295 bugs.extend(match[0].replace(' ', '').split(','))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100296 # Add default chromium: prefix if none specified.
297 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000298
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000299 return sorted(set(bugs))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000300
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000301 def process_rietveld_issue(self, remote, instance, issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000302 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000303 if self.options.deltas:
304 patchset_props = remote.get_patchset_properties(
305 issue['issue'],
306 issue['patchsets'][-1])
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100307 self.show_progress()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000308 ret['delta'] = '+%d,-%d' % (
309 sum(f['num_added'] for f in patchset_props['files'].itervalues()),
310 sum(f['num_removed'] for f in patchset_props['files'].itervalues()))
311
312 if issue['landed_days_ago'] != 'unknown':
313 ret['status'] = 'committed'
314 elif issue['closed']:
315 ret['status'] = 'closed'
316 elif len(issue['reviewers']) and issue['all_required_reviewers_approved']:
317 ret['status'] = 'ready'
318 else:
319 ret['status'] = 'open'
320
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000321 ret['owner'] = issue['owner_email']
322 ret['author'] = ret['owner']
323
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000324 ret['reviewers'] = set(issue['reviewers'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000325
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000326 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700327 url = instance['shorturl']
328 protocol = instance.get('short_url_protocol', 'http')
329 else:
330 url = instance['url']
331 protocol = 'https'
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000332
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700333 ret['review_url'] = '%s://%s/%d' % (protocol, url, issue['issue'])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000334
335 # Rietveld sometimes has '\r\n' instead of '\n'.
336 ret['header'] = issue['description'].replace('\r', '').split('\n')[0]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000337
338 ret['modified'] = datetime_from_rietveld(issue['modified'])
339 ret['created'] = datetime_from_rietveld(issue['created'])
340 ret['replies'] = self.process_rietveld_replies(issue['messages'])
341
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000342 ret['bugs'] = self.extract_bug_numbers_from_description(issue)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000343 ret['landed_days_ago'] = issue['landed_days_ago']
344
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000345 return ret
346
347 @staticmethod
348 def process_rietveld_replies(replies):
349 ret = []
350 for reply in replies:
351 r = {}
352 r['author'] = reply['sender']
353 r['created'] = datetime_from_rietveld(reply['date'])
354 r['content'] = ''
355 ret.append(r)
356 return ret
357
Vadim Bendebury8de38002018-05-14 19:02:55 -0700358 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200359 # Convert the "key:value" filter to a list of (key, value) pairs.
360 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000361 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000362 # Instantiate the generator to force all the requests now and catch the
363 # errors here.
364 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000365 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
366 'CURRENT_REVISION', 'CURRENT_COMMIT']))
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000367 except gerrit_util.GerritError, e:
Vadim Bendebury8de38002018-05-14 19:02:55 -0700368 error_message = 'Looking up %r: %s' % (instance['url'], e)
369 if error_message not in self.access_errors:
370 self.access_errors.add(error_message)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000371 return []
372
deymo@chromium.org6c039202013-09-12 12:28:12 +0000373 def gerrit_search(self, instance, owner=None, reviewer=None):
374 max_age = datetime.today() - self.modified_after
375 max_age = max_age.days * 24 * 3600 + max_age.seconds
376 user_filter = 'owner:%s' % owner if owner else 'reviewer:%s' % reviewer
377 filters = ['-age:%ss' % max_age, user_filter]
Peter K. Lee9342ac02018-08-28 21:03:51 +0000378 # TODO(cjhopman): Should abandoned changes be filtered out when
379 # merged_only is not enabled?
380 if self.options.merged_only:
381 filters.append('status:merged')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000382
Aaron Gable2979a872017-09-05 17:38:32 -0700383 issues = self.gerrit_changes_over_rest(instance, filters)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100384 self.show_progress()
Aaron Gable2979a872017-09-05 17:38:32 -0700385 issues = [self.process_gerrit_issue(instance, issue)
386 for issue in issues]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000387
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000388 issues = filter(self.filter_issue, issues)
389 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
390
391 return issues
392
Aaron Gable2979a872017-09-05 17:38:32 -0700393 def process_gerrit_issue(self, instance, issue):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000394 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000395 if self.options.deltas:
396 ret['delta'] = DefaultFormatter().format(
397 '+{insertions},-{deletions}',
398 **issue)
399 ret['status'] = issue['status']
deymo@chromium.org6c039202013-09-12 12:28:12 +0000400 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700401 protocol = instance.get('short_url_protocol', 'http')
402 url = instance['shorturl']
403 else:
404 protocol = 'https'
405 url = instance['url']
406 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
407
deymo@chromium.org6c039202013-09-12 12:28:12 +0000408 ret['header'] = issue['subject']
Don Garrett2ebf9fd2018-08-06 15:14:00 +0000409 ret['owner'] = issue['owner'].get('email', '')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000410 ret['author'] = ret['owner']
411 ret['created'] = datetime_from_gerrit(issue['created'])
412 ret['modified'] = datetime_from_gerrit(issue['updated'])
413 if 'messages' in issue:
Aaron Gable2979a872017-09-05 17:38:32 -0700414 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
deymo@chromium.org6c039202013-09-12 12:28:12 +0000415 else:
416 ret['replies'] = []
417 ret['reviewers'] = set(r['author'] for r in ret['replies'])
418 ret['reviewers'].discard(ret['author'])
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000419 ret['bugs'] = self.extract_bug_numbers_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000420 return ret
421
422 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700423 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000424 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000425 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
426 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000427 for reply in replies:
428 ret.append({
429 'author': reply['author']['email'],
430 'created': datetime_from_gerrit(reply['date']),
431 'content': reply['message'],
432 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000433 return ret
434
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100435 def monorail_get_auth_http(self):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000436 auth_config = auth.extract_auth_config_from_options(self.options)
437 authenticator = auth.get_authenticator_for_host(
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000438 'bugs.chromium.org', auth_config)
Kenneth Russell66badbd2018-09-09 21:35:32 +0000439 # Manually use a long timeout (10m); for some users who have a
440 # long history on the issue tracker, whatever the default timeout
441 # is is reached.
442 return authenticator.authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100443
444 def filter_modified_monorail_issue(self, issue):
445 """Precisely checks if an issue has been modified in the time range.
446
447 This fetches all issue comments to check if the issue has been modified in
448 the time range specified by user. This is needed because monorail only
449 allows filtering by last updated and published dates, which is not
450 sufficient to tell whether a given issue has been modified at some specific
451 time range. Any update to the issue is a reported as comment on Monorail.
452
453 Args:
454 issue: Issue dict as returned by monorail_query_issues method. In
455 particular, must have a key 'uid' formatted as 'project:issue_id'.
456
457 Returns:
458 Passed issue if modified, None otherwise.
459 """
460 http = self.monorail_get_auth_http()
461 project, issue_id = issue['uid'].split(':')
462 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
463 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
464 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100465 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100466 content = json.loads(body)
467 if not content:
468 logging.error('Unable to parse %s response from monorail.', project)
469 return issue
470
471 for item in content.get('items', []):
472 comment_published = datetime_from_monorail(item['published'])
473 if self.filter_modified(comment_published):
474 return issue
475
476 return None
477
478 def monorail_query_issues(self, project, query):
479 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000480 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100481 '/%s/issues') % project
482 query_data = urllib.urlencode(query)
483 url = url + '?' + query_data
484 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100485 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100486 content = json.loads(body)
487 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100488 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100489 return []
490
491 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100492 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100493 for item in content.get('items', []):
494 if project_config.get('shorturl'):
495 protocol = project_config.get('short_url_protocol', 'http')
496 item_url = '%s://%s/%d' % (
497 protocol, project_config['shorturl'], item['id'])
498 else:
499 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
500 project, item['id'])
501 issue = {
502 'uid': '%s:%s' % (project, item['id']),
503 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100504 'created': datetime_from_monorail(item['published']),
505 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100506 'author': item['author']['name'],
507 'url': item_url,
508 'comments': [],
509 'status': item['status'],
510 'labels': [],
511 'components': []
512 }
513 if 'owner' in item:
514 issue['owner'] = item['owner']['name']
515 else:
516 issue['owner'] = 'None'
517 if 'labels' in item:
518 issue['labels'] = item['labels']
519 if 'components' in item:
520 issue['components'] = item['components']
521 issues.append(issue)
522
523 return issues
524
525 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000526 epoch = datetime.utcfromtimestamp(0)
527 user_str = '%s@chromium.org' % self.user
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000528
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100529 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000530 'maxResults': 10000,
531 'q': user_str,
532 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
533 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000534 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000535
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100536 return [
537 issue for issue in issues
538 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000539
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100540 def monorail_get_issues(self, project, issue_ids):
541 return self.monorail_query_issues(project, {
542 'maxResults': 10000,
543 'q': 'id:%s' % ','.join(issue_ids)
544 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000545
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000546 def print_heading(self, heading):
547 print
548 print self.options.output_format_heading.format(heading=heading)
549
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000550 def match(self, author):
551 if '@' in self.user:
552 return author == self.user
553 return author.startswith(self.user + '@')
554
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000555 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000556 activity = len([
557 reply
558 for reply in change['replies']
559 if self.match(reply['author'])
560 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000561 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000562 'created': change['created'].date().isoformat(),
563 'modified': change['modified'].date().isoformat(),
564 'reviewers': ', '.join(change['reviewers']),
565 'status': change['status'],
566 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000567 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000568 if self.options.deltas:
569 optional_values['delta'] = change['delta']
570
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000571 self.print_generic(self.options.output_format,
572 self.options.output_format_changes,
573 change['header'],
574 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000575 change['author'],
576 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000577
578 def print_issue(self, issue):
579 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000580 'created': issue['created'].date().isoformat(),
581 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000582 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000583 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000584 }
585 self.print_generic(self.options.output_format,
586 self.options.output_format_issues,
587 issue['header'],
588 issue['url'],
589 issue['author'],
590 optional_values)
591
592 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000593 activity = len([
594 reply
595 for reply in review['replies']
596 if self.match(reply['author'])
597 ])
598 optional_values = {
599 'created': review['created'].date().isoformat(),
600 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800601 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000602 'activity': activity,
603 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800604 if self.options.deltas:
605 optional_values['delta'] = review['delta']
606
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000607 self.print_generic(self.options.output_format,
608 self.options.output_format_reviews,
609 review['header'],
610 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000611 review['author'],
612 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000613
614 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000615 def print_generic(default_fmt, specific_fmt,
616 title, url, author,
617 optional_values=None):
618 output_format = specific_fmt if specific_fmt is not None else default_fmt
619 output_format = unicode(output_format)
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000620 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000621 'title': title,
622 'url': url,
623 'author': author,
624 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000625 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000626 values.update(optional_values)
627 print DefaultFormatter().format(output_format, **values).encode(
628 sys.getdefaultencoding())
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000629
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000630
631 def filter_issue(self, issue, should_filter_by_user=True):
632 def maybe_filter_username(email):
633 return not should_filter_by_user or username(email) == self.user
634 if (maybe_filter_username(issue['author']) and
635 self.filter_modified(issue['created'])):
636 return True
637 if (maybe_filter_username(issue['owner']) and
638 (self.filter_modified(issue['created']) or
639 self.filter_modified(issue['modified']))):
640 return True
641 for reply in issue['replies']:
642 if self.filter_modified(reply['created']):
643 if not should_filter_by_user:
644 break
645 if (username(reply['author']) == self.user
646 or (self.user + '@') in reply['content']):
647 break
648 else:
649 return False
650 return True
651
652 def filter_modified(self, modified):
653 return self.modified_after < modified and modified < self.modified_before
654
655 def auth_for_changes(self):
656 #TODO(cjhopman): Move authentication check for getting changes here.
657 pass
658
659 def auth_for_reviews(self):
660 # Reviews use all the same instances as changes so no authentication is
661 # required.
662 pass
663
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000664 def get_changes(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100665 num_instances = len(rietveld_instances) + len(gerrit_instances)
666 with contextlib.closing(ThreadPool(num_instances)) as pool:
667 rietveld_changes = pool.map_async(
668 lambda instance: self.rietveld_search(instance, owner=self.user),
669 rietveld_instances)
670 gerrit_changes = pool.map_async(
671 lambda instance: self.gerrit_search(instance, owner=self.user),
672 gerrit_instances)
673 rietveld_changes = itertools.chain.from_iterable(rietveld_changes.get())
674 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
675 self.changes = list(rietveld_changes) + list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000676
677 def print_changes(self):
678 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000679 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000680 for change in self.changes:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100681 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000682
Vadim Bendebury8de38002018-05-14 19:02:55 -0700683 def print_access_errors(self):
684 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400685 logging.error('Access Errors:')
686 for error in self.access_errors:
687 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700688
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000689 def get_reviews(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100690 num_instances = len(rietveld_instances) + len(gerrit_instances)
691 with contextlib.closing(ThreadPool(num_instances)) as pool:
692 rietveld_reviews = pool.map_async(
693 lambda instance: self.rietveld_search(instance, reviewer=self.user),
694 rietveld_instances)
695 gerrit_reviews = pool.map_async(
696 lambda instance: self.gerrit_search(instance, reviewer=self.user),
697 gerrit_instances)
698 rietveld_reviews = itertools.chain.from_iterable(rietveld_reviews.get())
699 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
jdoerrie356c2882018-07-23 10:02:02 +0000700 gerrit_reviews = [r for r in gerrit_reviews if not self.match(r['owner'])]
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100701 self.reviews = list(rietveld_reviews) + list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000702
703 def print_reviews(self):
704 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000705 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000706 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000707 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000708
709 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100710 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
711 monorail_issues = pool.map(
712 self.monorail_issue_search, monorail_projects.keys())
713 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
714
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700715 if not monorail_issues:
716 return
717
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100718 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
719 filtered_issues = pool.map(
720 self.filter_modified_monorail_issue, monorail_issues)
721 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100722
723 def get_referenced_issues(self):
724 if not self.issues:
725 self.get_issues()
726
727 if not self.changes:
728 self.get_changes()
729
730 referenced_issue_uids = set(itertools.chain.from_iterable(
731 change['bugs'] for change in self.changes))
732 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
733 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
734
735 missing_issues_by_project = collections.defaultdict(list)
736 for issue_uid in missing_issue_uids:
737 project, issue_id = issue_uid.split(':')
738 missing_issues_by_project[project].append(issue_id)
739
740 for project, issue_ids in missing_issues_by_project.iteritems():
741 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000742
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000743 def print_issues(self):
744 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000745 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000746 for issue in self.issues:
747 self.print_issue(issue)
748
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100749 def print_changes_by_issue(self, skip_empty_own):
750 if not self.issues or not self.changes:
751 return
752
753 self.print_heading('Changes by referenced issue(s)')
754 issues = {issue['uid']: issue for issue in self.issues}
755 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
756 changes_by_issue_uid = collections.defaultdict(list)
757 changes_by_ref_issue_uid = collections.defaultdict(list)
758 changes_without_issue = []
759 for change in self.changes:
760 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000761 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100762 if issue_uid in issues:
763 changes_by_issue_uid[issue_uid].append(change)
764 added = True
765 if issue_uid in ref_issues:
766 changes_by_ref_issue_uid[issue_uid].append(change)
767 added = True
768 if not added:
769 changes_without_issue.append(change)
770
771 # Changes referencing own issues.
772 for issue_uid in issues:
773 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
774 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000775 if changes_by_issue_uid[issue_uid]:
776 print
777 for change in changes_by_issue_uid[issue_uid]:
778 print ' ', # this prints one space due to comma, but no newline
779 self.print_change(change)
780 print
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100781
782 # Changes referencing others' issues.
783 for issue_uid in ref_issues:
784 assert changes_by_ref_issue_uid[issue_uid]
785 self.print_issue(ref_issues[issue_uid])
786 for change in changes_by_ref_issue_uid[issue_uid]:
787 print '', # this prints one space due to comma, but no newline
788 self.print_change(change)
789
790 # Changes referencing no issues.
791 if changes_without_issue:
792 print self.options.output_format_no_url.format(title='Other changes')
793 for change in changes_without_issue:
794 print '', # this prints one space due to comma, but no newline
795 self.print_change(change)
796
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000797 def print_activity(self):
798 self.print_changes()
799 self.print_reviews()
800 self.print_issues()
801
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000802 def dump_json(self, ignore_keys=None):
803 if ignore_keys is None:
804 ignore_keys = ['replies']
805
806 def format_for_json_dump(in_array):
807 output = {}
808 for item in in_array:
809 url = item.get('url') or item.get('review_url')
810 if not url:
811 raise Exception('Dumped item %s does not specify url' % item)
812 output[url] = dict(
813 (k, v) for k,v in item.iteritems() if k not in ignore_keys)
814 return output
815
816 class PythonObjectEncoder(json.JSONEncoder):
817 def default(self, obj): # pylint: disable=method-hidden
818 if isinstance(obj, datetime):
819 return obj.isoformat()
820 if isinstance(obj, set):
821 return list(obj)
822 return json.JSONEncoder.default(self, obj)
823
824 output = {
825 'reviews': format_for_json_dump(self.reviews),
826 'changes': format_for_json_dump(self.changes),
827 'issues': format_for_json_dump(self.issues)
828 }
829 print json.dumps(output, indent=2, cls=PythonObjectEncoder)
830
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000831
832def main():
833 # Silence upload.py.
834 rietveld.upload.verbosity = 0
835
836 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
837 parser.add_option(
838 '-u', '--user', metavar='<email>',
839 default=os.environ.get('USER'),
840 help='Filter on user, default=%default')
841 parser.add_option(
842 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000843 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000844 parser.add_option(
845 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000846 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000847 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
848 relativedelta(months=2))
849 parser.add_option(
850 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000851 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000852 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
853 parser.add_option(
854 '-Y', '--this_year', action='store_true',
855 help='Use this year\'s dates')
856 parser.add_option(
857 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000858 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000859 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000860 '-W', '--last_week', action='count',
861 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000862 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000863 '-a', '--auth',
864 action='store_true',
865 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000866 parser.add_option(
867 '-d', '--deltas',
868 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800869 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100870 parser.add_option(
871 '--no-referenced-issues',
872 action='store_true',
873 help='Do not fetch issues referenced by owned changes. Useful in '
874 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100875 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100876 parser.add_option(
877 '--skip-own-issues-without-changes',
878 action='store_true',
879 help='Skips listing own issues without changes when showing changes '
880 'grouped by referenced issue(s). See --changes-by-issue for more '
881 'details.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000882
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000883 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000884 'By default, all activity will be looked up and '
885 'printed. If any of these are specified, only '
886 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000887 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000888 '-c', '--changes',
889 action='store_true',
890 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000891 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000892 '-i', '--issues',
893 action='store_true',
894 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000895 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000896 '-r', '--reviews',
897 action='store_true',
898 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100899 activity_types_group.add_option(
900 '--changes-by-issue', action='store_true',
901 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000902 parser.add_option_group(activity_types_group)
903
904 output_format_group = optparse.OptionGroup(parser, 'Output Format',
905 'By default, all activity will be printed in the '
906 'following format: {url} {title}. This can be '
907 'changed for either all activity types or '
908 'individually for each activity type. The format '
909 'is defined as documented for '
910 'string.format(...). The variables available for '
911 'all activity types are url, title and author. '
912 'Format options for specific activity types will '
913 'override the generic format.')
914 output_format_group.add_option(
915 '-f', '--output-format', metavar='<format>',
916 default=u'{url} {title}',
917 help='Specifies the format to use when printing all your activity.')
918 output_format_group.add_option(
919 '--output-format-changes', metavar='<format>',
920 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000921 help='Specifies the format to use when printing changes. Supports the '
922 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000923 output_format_group.add_option(
924 '--output-format-issues', metavar='<format>',
925 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000926 help='Specifies the format to use when printing issues. Supports the '
927 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000928 output_format_group.add_option(
929 '--output-format-reviews', metavar='<format>',
930 default=None,
931 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000932 output_format_group.add_option(
933 '--output-format-heading', metavar='<format>',
934 default=u'{heading}:',
935 help='Specifies the format to use when printing headings.')
936 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100937 '--output-format-no-url', default='{title}',
938 help='Specifies the format to use when printing activity without url.')
939 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000940 '-m', '--markdown', action='store_true',
941 help='Use markdown-friendly output (overrides --output-format '
942 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000943 output_format_group.add_option(
944 '-j', '--json', action='store_true',
945 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000946 parser.add_option_group(output_format_group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000947 auth.add_auth_options(parser)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000948
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000949 parser.add_option(
950 '-v', '--verbose',
951 action='store_const',
952 dest='verbosity',
953 default=logging.WARN,
954 const=logging.INFO,
955 help='Output extra informational messages.'
956 )
957 parser.add_option(
958 '-q', '--quiet',
959 action='store_const',
960 dest='verbosity',
961 const=logging.ERROR,
962 help='Suppress non-error messages.'
963 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000964 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000965 '-M', '--merged-only',
966 action='store_true',
967 dest='merged_only',
968 default=False,
969 help='Shows only changes that have been merged.')
970 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000971 '-o', '--output', metavar='<file>',
972 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000973
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000974 # Remove description formatting
975 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800976 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000977
978 options, args = parser.parse_args()
979 options.local_user = os.environ.get('USER')
980 if args:
981 parser.error('Args unsupported')
982 if not options.user:
983 parser.error('USER is not set, please use -u')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000984 options.user = username(options.user)
985
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000986 logging.basicConfig(level=options.verbosity)
987
988 # python-keyring provides easy access to the system keyring.
989 try:
990 import keyring # pylint: disable=unused-import,unused-variable,F0401
991 except ImportError:
992 logging.warning('Consider installing python-keyring')
993
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000994 if not options.begin:
995 if options.last_quarter:
996 begin, end = quarter_begin, quarter_end
997 elif options.this_year:
998 begin, end = get_year_of(datetime.today())
999 elif options.week_of:
1000 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +00001001 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +00001002 begin, end = (get_week_of(datetime.today() -
1003 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001004 else:
1005 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
1006 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -07001007 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001008 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -07001009 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001010 else:
1011 end = datetime.today()
1012 options.begin, options.end = begin, end
1013
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +00001014 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +00001015 options.output_format_heading = '### {heading}\n'
1016 options.output_format = ' * [{title}]({url})'
1017 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +00001018 logging.info('Searching for activity by %s', options.user)
1019 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001020
1021 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +01001022 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001023
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001024 if not (options.changes or options.reviews or options.issues or
1025 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001026 options.changes = True
1027 options.issues = True
1028 options.reviews = True
1029
1030 # First do any required authentication so none of the user interaction has to
1031 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001032 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001033 my_activity.auth_for_changes()
1034 if options.reviews:
1035 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001036
Tobias Sargeantffb3c432017-03-08 14:09:14 +00001037 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001038
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001039 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001040 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001041 my_activity.get_changes()
1042 if options.reviews:
1043 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001044 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001045 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001046 if not options.no_referenced_issues:
1047 my_activity.get_referenced_issues()
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001048 except auth.AuthenticationError as e:
Tobias Sargeantffb3c432017-03-08 14:09:14 +00001049 logging.error('auth.AuthenticationError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001050
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +01001051 my_activity.show_progress('\n')
1052
Vadim Bendebury8de38002018-05-14 19:02:55 -07001053 my_activity.print_access_errors()
1054
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +00001055 output_file = None
1056 try:
1057 if options.output:
1058 output_file = open(options.output, 'w')
1059 logging.info('Printing output to "%s"', options.output)
1060 sys.stdout = output_file
1061 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -07001062 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +00001063 else:
1064 if options.json:
1065 my_activity.dump_json()
1066 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001067 if options.changes:
1068 my_activity.print_changes()
1069 if options.reviews:
1070 my_activity.print_reviews()
1071 if options.issues:
1072 my_activity.print_issues()
1073 if options.changes_by_issue:
1074 my_activity.print_changes_by_issue(
1075 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +00001076 finally:
1077 if output_file:
1078 logging.info('Done printing to file.')
1079 sys.stdout = sys.__stdout__
1080 output_file.close()
1081
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001082 return 0
1083
1084
1085if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +00001086 # Fix encoding to support non-ascii issue titles.
1087 fix_encoding.fix_encoding()
1088
sbc@chromium.org013731e2015-02-26 18:28:43 +00001089 try:
1090 sys.exit(main())
1091 except KeyboardInterrupt:
1092 sys.stderr.write('interrupted\n')
1093 sys.exit(1)