blob: b732b0d845dd505ed0f2b7d9a65d0c428f01cd5a [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
Andrii Shyshkalov4aedec02018-09-17 16:31:17 +0000375 filters = ['-age:%ss' % (max_age.days * 24 * 3600 + max_age.seconds)]
376 if owner:
377 assert not reviewer
378 filters.append('owner:%s' % owner)
379 else:
380 filters.extend(('-owner:%s' % reviewer, 'reviewer:%s' % reviewer))
Peter K. Lee9342ac02018-08-28 21:03:51 +0000381 # TODO(cjhopman): Should abandoned changes be filtered out when
382 # merged_only is not enabled?
383 if self.options.merged_only:
384 filters.append('status:merged')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000385
Aaron Gable2979a872017-09-05 17:38:32 -0700386 issues = self.gerrit_changes_over_rest(instance, filters)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100387 self.show_progress()
Aaron Gable2979a872017-09-05 17:38:32 -0700388 issues = [self.process_gerrit_issue(instance, issue)
389 for issue in issues]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000390
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000391 issues = filter(self.filter_issue, issues)
392 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
393
394 return issues
395
Aaron Gable2979a872017-09-05 17:38:32 -0700396 def process_gerrit_issue(self, instance, issue):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000397 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000398 if self.options.deltas:
399 ret['delta'] = DefaultFormatter().format(
400 '+{insertions},-{deletions}',
401 **issue)
402 ret['status'] = issue['status']
deymo@chromium.org6c039202013-09-12 12:28:12 +0000403 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700404 protocol = instance.get('short_url_protocol', 'http')
405 url = instance['shorturl']
406 else:
407 protocol = 'https'
408 url = instance['url']
409 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
410
deymo@chromium.org6c039202013-09-12 12:28:12 +0000411 ret['header'] = issue['subject']
Don Garrett2ebf9fd2018-08-06 15:14:00 +0000412 ret['owner'] = issue['owner'].get('email', '')
deymo@chromium.org6c039202013-09-12 12:28:12 +0000413 ret['author'] = ret['owner']
414 ret['created'] = datetime_from_gerrit(issue['created'])
415 ret['modified'] = datetime_from_gerrit(issue['updated'])
416 if 'messages' in issue:
Aaron Gable2979a872017-09-05 17:38:32 -0700417 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
deymo@chromium.org6c039202013-09-12 12:28:12 +0000418 else:
419 ret['replies'] = []
420 ret['reviewers'] = set(r['author'] for r in ret['replies'])
421 ret['reviewers'].discard(ret['author'])
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000422 ret['bugs'] = self.extract_bug_numbers_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000423 return ret
424
425 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700426 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000427 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000428 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
429 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000430 for reply in replies:
431 ret.append({
432 'author': reply['author']['email'],
433 'created': datetime_from_gerrit(reply['date']),
434 'content': reply['message'],
435 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000436 return ret
437
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100438 def monorail_get_auth_http(self):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000439 auth_config = auth.extract_auth_config_from_options(self.options)
440 authenticator = auth.get_authenticator_for_host(
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000441 'bugs.chromium.org', auth_config)
Kenneth Russell66badbd2018-09-09 21:35:32 +0000442 # Manually use a long timeout (10m); for some users who have a
443 # long history on the issue tracker, whatever the default timeout
444 # is is reached.
445 return authenticator.authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100446
447 def filter_modified_monorail_issue(self, issue):
448 """Precisely checks if an issue has been modified in the time range.
449
450 This fetches all issue comments to check if the issue has been modified in
451 the time range specified by user. This is needed because monorail only
452 allows filtering by last updated and published dates, which is not
453 sufficient to tell whether a given issue has been modified at some specific
454 time range. Any update to the issue is a reported as comment on Monorail.
455
456 Args:
457 issue: Issue dict as returned by monorail_query_issues method. In
458 particular, must have a key 'uid' formatted as 'project:issue_id'.
459
460 Returns:
461 Passed issue if modified, None otherwise.
462 """
463 http = self.monorail_get_auth_http()
464 project, issue_id = issue['uid'].split(':')
465 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
466 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
467 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100468 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100469 content = json.loads(body)
470 if not content:
471 logging.error('Unable to parse %s response from monorail.', project)
472 return issue
473
474 for item in content.get('items', []):
475 comment_published = datetime_from_monorail(item['published'])
476 if self.filter_modified(comment_published):
477 return issue
478
479 return None
480
481 def monorail_query_issues(self, project, query):
482 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000483 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100484 '/%s/issues') % project
485 query_data = urllib.urlencode(query)
486 url = url + '?' + query_data
487 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100488 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100489 content = json.loads(body)
490 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100491 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100492 return []
493
494 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100495 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100496 for item in content.get('items', []):
497 if project_config.get('shorturl'):
498 protocol = project_config.get('short_url_protocol', 'http')
499 item_url = '%s://%s/%d' % (
500 protocol, project_config['shorturl'], item['id'])
501 else:
502 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
503 project, item['id'])
504 issue = {
505 'uid': '%s:%s' % (project, item['id']),
506 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100507 'created': datetime_from_monorail(item['published']),
508 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100509 'author': item['author']['name'],
510 'url': item_url,
511 'comments': [],
512 'status': item['status'],
513 'labels': [],
514 'components': []
515 }
516 if 'owner' in item:
517 issue['owner'] = item['owner']['name']
518 else:
519 issue['owner'] = 'None'
520 if 'labels' in item:
521 issue['labels'] = item['labels']
522 if 'components' in item:
523 issue['components'] = item['components']
524 issues.append(issue)
525
526 return issues
527
528 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000529 epoch = datetime.utcfromtimestamp(0)
530 user_str = '%s@chromium.org' % self.user
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000531
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100532 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000533 'maxResults': 10000,
534 'q': user_str,
535 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
536 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000537 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000538
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100539 return [
540 issue for issue in issues
541 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000542
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100543 def monorail_get_issues(self, project, issue_ids):
544 return self.monorail_query_issues(project, {
545 'maxResults': 10000,
546 'q': 'id:%s' % ','.join(issue_ids)
547 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000548
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000549 def print_heading(self, heading):
550 print
551 print self.options.output_format_heading.format(heading=heading)
552
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000553 def match(self, author):
554 if '@' in self.user:
555 return author == self.user
556 return author.startswith(self.user + '@')
557
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000558 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000559 activity = len([
560 reply
561 for reply in change['replies']
562 if self.match(reply['author'])
563 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000564 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000565 'created': change['created'].date().isoformat(),
566 'modified': change['modified'].date().isoformat(),
567 'reviewers': ', '.join(change['reviewers']),
568 'status': change['status'],
569 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000570 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000571 if self.options.deltas:
572 optional_values['delta'] = change['delta']
573
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000574 self.print_generic(self.options.output_format,
575 self.options.output_format_changes,
576 change['header'],
577 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000578 change['author'],
579 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000580
581 def print_issue(self, issue):
582 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000583 'created': issue['created'].date().isoformat(),
584 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000585 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000586 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000587 }
588 self.print_generic(self.options.output_format,
589 self.options.output_format_issues,
590 issue['header'],
591 issue['url'],
592 issue['author'],
593 optional_values)
594
595 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000596 activity = len([
597 reply
598 for reply in review['replies']
599 if self.match(reply['author'])
600 ])
601 optional_values = {
602 'created': review['created'].date().isoformat(),
603 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800604 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000605 'activity': activity,
606 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800607 if self.options.deltas:
608 optional_values['delta'] = review['delta']
609
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000610 self.print_generic(self.options.output_format,
611 self.options.output_format_reviews,
612 review['header'],
613 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000614 review['author'],
615 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000616
617 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000618 def print_generic(default_fmt, specific_fmt,
619 title, url, author,
620 optional_values=None):
621 output_format = specific_fmt if specific_fmt is not None else default_fmt
622 output_format = unicode(output_format)
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000623 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000624 'title': title,
625 'url': url,
626 'author': author,
627 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000628 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000629 values.update(optional_values)
630 print DefaultFormatter().format(output_format, **values).encode(
631 sys.getdefaultencoding())
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000632
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000633
634 def filter_issue(self, issue, should_filter_by_user=True):
635 def maybe_filter_username(email):
636 return not should_filter_by_user or username(email) == self.user
637 if (maybe_filter_username(issue['author']) and
638 self.filter_modified(issue['created'])):
639 return True
640 if (maybe_filter_username(issue['owner']) and
641 (self.filter_modified(issue['created']) or
642 self.filter_modified(issue['modified']))):
643 return True
644 for reply in issue['replies']:
645 if self.filter_modified(reply['created']):
646 if not should_filter_by_user:
647 break
648 if (username(reply['author']) == self.user
649 or (self.user + '@') in reply['content']):
650 break
651 else:
652 return False
653 return True
654
655 def filter_modified(self, modified):
656 return self.modified_after < modified and modified < self.modified_before
657
658 def auth_for_changes(self):
659 #TODO(cjhopman): Move authentication check for getting changes here.
660 pass
661
662 def auth_for_reviews(self):
663 # Reviews use all the same instances as changes so no authentication is
664 # required.
665 pass
666
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000667 def get_changes(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100668 num_instances = len(rietveld_instances) + len(gerrit_instances)
669 with contextlib.closing(ThreadPool(num_instances)) as pool:
670 rietveld_changes = pool.map_async(
671 lambda instance: self.rietveld_search(instance, owner=self.user),
672 rietveld_instances)
673 gerrit_changes = pool.map_async(
674 lambda instance: self.gerrit_search(instance, owner=self.user),
675 gerrit_instances)
676 rietveld_changes = itertools.chain.from_iterable(rietveld_changes.get())
677 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
678 self.changes = list(rietveld_changes) + list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000679
680 def print_changes(self):
681 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000682 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000683 for change in self.changes:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100684 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000685
Vadim Bendebury8de38002018-05-14 19:02:55 -0700686 def print_access_errors(self):
687 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400688 logging.error('Access Errors:')
689 for error in self.access_errors:
690 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700691
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000692 def get_reviews(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100693 num_instances = len(rietveld_instances) + len(gerrit_instances)
694 with contextlib.closing(ThreadPool(num_instances)) as pool:
695 rietveld_reviews = pool.map_async(
696 lambda instance: self.rietveld_search(instance, reviewer=self.user),
697 rietveld_instances)
698 gerrit_reviews = pool.map_async(
699 lambda instance: self.gerrit_search(instance, reviewer=self.user),
700 gerrit_instances)
701 rietveld_reviews = itertools.chain.from_iterable(rietveld_reviews.get())
702 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100703 self.reviews = list(rietveld_reviews) + list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000704
705 def print_reviews(self):
706 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000707 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000708 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000709 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000710
711 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100712 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
713 monorail_issues = pool.map(
714 self.monorail_issue_search, monorail_projects.keys())
715 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
716
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700717 if not monorail_issues:
718 return
719
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100720 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
721 filtered_issues = pool.map(
722 self.filter_modified_monorail_issue, monorail_issues)
723 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100724
725 def get_referenced_issues(self):
726 if not self.issues:
727 self.get_issues()
728
729 if not self.changes:
730 self.get_changes()
731
732 referenced_issue_uids = set(itertools.chain.from_iterable(
733 change['bugs'] for change in self.changes))
734 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
735 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
736
737 missing_issues_by_project = collections.defaultdict(list)
738 for issue_uid in missing_issue_uids:
739 project, issue_id = issue_uid.split(':')
740 missing_issues_by_project[project].append(issue_id)
741
742 for project, issue_ids in missing_issues_by_project.iteritems():
743 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000744
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000745 def print_issues(self):
746 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000747 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000748 for issue in self.issues:
749 self.print_issue(issue)
750
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100751 def print_changes_by_issue(self, skip_empty_own):
752 if not self.issues or not self.changes:
753 return
754
755 self.print_heading('Changes by referenced issue(s)')
756 issues = {issue['uid']: issue for issue in self.issues}
757 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
758 changes_by_issue_uid = collections.defaultdict(list)
759 changes_by_ref_issue_uid = collections.defaultdict(list)
760 changes_without_issue = []
761 for change in self.changes:
762 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000763 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100764 if issue_uid in issues:
765 changes_by_issue_uid[issue_uid].append(change)
766 added = True
767 if issue_uid in ref_issues:
768 changes_by_ref_issue_uid[issue_uid].append(change)
769 added = True
770 if not added:
771 changes_without_issue.append(change)
772
773 # Changes referencing own issues.
774 for issue_uid in issues:
775 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
776 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000777 if changes_by_issue_uid[issue_uid]:
778 print
779 for change in changes_by_issue_uid[issue_uid]:
780 print ' ', # this prints one space due to comma, but no newline
781 self.print_change(change)
782 print
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100783
784 # Changes referencing others' issues.
785 for issue_uid in ref_issues:
786 assert changes_by_ref_issue_uid[issue_uid]
787 self.print_issue(ref_issues[issue_uid])
788 for change in changes_by_ref_issue_uid[issue_uid]:
789 print '', # this prints one space due to comma, but no newline
790 self.print_change(change)
791
792 # Changes referencing no issues.
793 if changes_without_issue:
794 print self.options.output_format_no_url.format(title='Other changes')
795 for change in changes_without_issue:
796 print '', # this prints one space due to comma, but no newline
797 self.print_change(change)
798
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000799 def print_activity(self):
800 self.print_changes()
801 self.print_reviews()
802 self.print_issues()
803
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000804 def dump_json(self, ignore_keys=None):
805 if ignore_keys is None:
806 ignore_keys = ['replies']
807
808 def format_for_json_dump(in_array):
809 output = {}
810 for item in in_array:
811 url = item.get('url') or item.get('review_url')
812 if not url:
813 raise Exception('Dumped item %s does not specify url' % item)
814 output[url] = dict(
815 (k, v) for k,v in item.iteritems() if k not in ignore_keys)
816 return output
817
818 class PythonObjectEncoder(json.JSONEncoder):
819 def default(self, obj): # pylint: disable=method-hidden
820 if isinstance(obj, datetime):
821 return obj.isoformat()
822 if isinstance(obj, set):
823 return list(obj)
824 return json.JSONEncoder.default(self, obj)
825
826 output = {
827 'reviews': format_for_json_dump(self.reviews),
828 'changes': format_for_json_dump(self.changes),
829 'issues': format_for_json_dump(self.issues)
830 }
831 print json.dumps(output, indent=2, cls=PythonObjectEncoder)
832
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000833
834def main():
835 # Silence upload.py.
836 rietveld.upload.verbosity = 0
837
838 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
839 parser.add_option(
840 '-u', '--user', metavar='<email>',
841 default=os.environ.get('USER'),
842 help='Filter on user, default=%default')
843 parser.add_option(
844 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000845 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000846 parser.add_option(
847 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000848 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000849 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
850 relativedelta(months=2))
851 parser.add_option(
852 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000853 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000854 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
855 parser.add_option(
856 '-Y', '--this_year', action='store_true',
857 help='Use this year\'s dates')
858 parser.add_option(
859 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000860 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000861 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000862 '-W', '--last_week', action='count',
863 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000864 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000865 '-a', '--auth',
866 action='store_true',
867 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000868 parser.add_option(
869 '-d', '--deltas',
870 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800871 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100872 parser.add_option(
873 '--no-referenced-issues',
874 action='store_true',
875 help='Do not fetch issues referenced by owned changes. Useful in '
876 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100877 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100878 parser.add_option(
879 '--skip-own-issues-without-changes',
880 action='store_true',
881 help='Skips listing own issues without changes when showing changes '
882 'grouped by referenced issue(s). See --changes-by-issue for more '
883 'details.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000884
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000885 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000886 'By default, all activity will be looked up and '
887 'printed. If any of these are specified, only '
888 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000889 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000890 '-c', '--changes',
891 action='store_true',
892 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000893 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000894 '-i', '--issues',
895 action='store_true',
896 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000897 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000898 '-r', '--reviews',
899 action='store_true',
900 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100901 activity_types_group.add_option(
902 '--changes-by-issue', action='store_true',
903 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000904 parser.add_option_group(activity_types_group)
905
906 output_format_group = optparse.OptionGroup(parser, 'Output Format',
907 'By default, all activity will be printed in the '
908 'following format: {url} {title}. This can be '
909 'changed for either all activity types or '
910 'individually for each activity type. The format '
911 'is defined as documented for '
912 'string.format(...). The variables available for '
913 'all activity types are url, title and author. '
914 'Format options for specific activity types will '
915 'override the generic format.')
916 output_format_group.add_option(
917 '-f', '--output-format', metavar='<format>',
918 default=u'{url} {title}',
919 help='Specifies the format to use when printing all your activity.')
920 output_format_group.add_option(
921 '--output-format-changes', metavar='<format>',
922 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000923 help='Specifies the format to use when printing changes. Supports the '
924 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000925 output_format_group.add_option(
926 '--output-format-issues', metavar='<format>',
927 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000928 help='Specifies the format to use when printing issues. Supports the '
929 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000930 output_format_group.add_option(
931 '--output-format-reviews', metavar='<format>',
932 default=None,
933 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000934 output_format_group.add_option(
935 '--output-format-heading', metavar='<format>',
936 default=u'{heading}:',
937 help='Specifies the format to use when printing headings.')
938 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100939 '--output-format-no-url', default='{title}',
940 help='Specifies the format to use when printing activity without url.')
941 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000942 '-m', '--markdown', action='store_true',
943 help='Use markdown-friendly output (overrides --output-format '
944 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000945 output_format_group.add_option(
946 '-j', '--json', action='store_true',
947 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000948 parser.add_option_group(output_format_group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000949 auth.add_auth_options(parser)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000950
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000951 parser.add_option(
952 '-v', '--verbose',
953 action='store_const',
954 dest='verbosity',
955 default=logging.WARN,
956 const=logging.INFO,
957 help='Output extra informational messages.'
958 )
959 parser.add_option(
960 '-q', '--quiet',
961 action='store_const',
962 dest='verbosity',
963 const=logging.ERROR,
964 help='Suppress non-error messages.'
965 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000966 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000967 '-M', '--merged-only',
968 action='store_true',
969 dest='merged_only',
970 default=False,
971 help='Shows only changes that have been merged.')
972 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000973 '-o', '--output', metavar='<file>',
974 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000975
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000976 # Remove description formatting
977 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800978 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000979
980 options, args = parser.parse_args()
981 options.local_user = os.environ.get('USER')
982 if args:
983 parser.error('Args unsupported')
984 if not options.user:
985 parser.error('USER is not set, please use -u')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000986 options.user = username(options.user)
987
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000988 logging.basicConfig(level=options.verbosity)
989
990 # python-keyring provides easy access to the system keyring.
991 try:
992 import keyring # pylint: disable=unused-import,unused-variable,F0401
993 except ImportError:
994 logging.warning('Consider installing python-keyring')
995
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000996 if not options.begin:
997 if options.last_quarter:
998 begin, end = quarter_begin, quarter_end
999 elif options.this_year:
1000 begin, end = get_year_of(datetime.today())
1001 elif options.week_of:
1002 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +00001003 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +00001004 begin, end = (get_week_of(datetime.today() -
1005 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001006 else:
1007 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
1008 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -07001009 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001010 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -07001011 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001012 else:
1013 end = datetime.today()
1014 options.begin, options.end = begin, end
1015
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +00001016 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +00001017 options.output_format_heading = '### {heading}\n'
1018 options.output_format = ' * [{title}]({url})'
1019 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +00001020 logging.info('Searching for activity by %s', options.user)
1021 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001022
1023 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +01001024 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001025
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001026 if not (options.changes or options.reviews or options.issues or
1027 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001028 options.changes = True
1029 options.issues = True
1030 options.reviews = True
1031
1032 # First do any required authentication so none of the user interaction has to
1033 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001034 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001035 my_activity.auth_for_changes()
1036 if options.reviews:
1037 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001038
Tobias Sargeantffb3c432017-03-08 14:09:14 +00001039 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001040
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001041 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001042 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001043 my_activity.get_changes()
1044 if options.reviews:
1045 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001046 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001047 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001048 if not options.no_referenced_issues:
1049 my_activity.get_referenced_issues()
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001050 except auth.AuthenticationError as e:
Tobias Sargeantffb3c432017-03-08 14:09:14 +00001051 logging.error('auth.AuthenticationError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001052
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +01001053 my_activity.show_progress('\n')
1054
Vadim Bendebury8de38002018-05-14 19:02:55 -07001055 my_activity.print_access_errors()
1056
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +00001057 output_file = None
1058 try:
1059 if options.output:
1060 output_file = open(options.output, 'w')
1061 logging.info('Printing output to "%s"', options.output)
1062 sys.stdout = output_file
1063 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -07001064 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +00001065 else:
1066 if options.json:
1067 my_activity.dump_json()
1068 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001069 if options.changes:
1070 my_activity.print_changes()
1071 if options.reviews:
1072 my_activity.print_reviews()
1073 if options.issues:
1074 my_activity.print_issues()
1075 if options.changes_by_issue:
1076 my_activity.print_changes_by_issue(
1077 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +00001078 finally:
1079 if output_file:
1080 logging.info('Done printing to file.')
1081 sys.stdout = sys.__stdout__
1082 output_file.close()
1083
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001084 return 0
1085
1086
1087if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +00001088 # Fix encoding to support non-ascii issue titles.
1089 fix_encoding.fix_encoding()
1090
sbc@chromium.org013731e2015-02-26 18:28:43 +00001091 try:
1092 sys.exit(main())
1093 except KeyboardInterrupt:
1094 sys.stderr.write('interrupted\n')
1095 sys.exit(1)