blob: be36c7432526e8265eefa24e4ac2fb73ae12aa85 [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 {
112 'url': 'pdfium-review.googlesource.com',
113 },
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000114]
115
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100116monorail_projects = {
117 'chromium': {
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000118 'shorturl': 'crbug.com',
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700119 'short_url_protocol': 'https',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000120 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100121 'google-breakpad': {},
122 'gyp': {},
123 'skia': {},
124 'pdfium': {
Ryan Harrison897602a2017-09-18 16:23:41 -0400125 'shorturl': 'crbug.com/pdfium',
126 'short_url_protocol': 'https',
127 },
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100128 'v8': {
129 'shorturl': 'crbug.com/v8',
130 'short_url_protocol': 'https',
131 },
132}
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000133
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000134def username(email):
135 """Keeps the username of an email address."""
136 return email and email.split('@', 1)[0]
137
138
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000139def datetime_to_midnight(date):
140 return date - timedelta(hours=date.hour, minutes=date.minute,
141 seconds=date.second, microseconds=date.microsecond)
142
143
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000144def get_quarter_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000145 begin = (datetime_to_midnight(date) -
146 relativedelta(months=(date.month % 3) - 1, days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000147 return begin, begin + relativedelta(months=3)
148
149
150def get_year_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000151 begin = (datetime_to_midnight(date) -
152 relativedelta(months=(date.month - 1), days=(date.day - 1)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000153 return begin, begin + relativedelta(years=1)
154
155
156def get_week_of(date):
cjhopman@chromium.org426557a2012-10-22 20:18:52 +0000157 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000158 return begin, begin + timedelta(days=7)
159
160
161def get_yes_or_no(msg):
162 while True:
163 response = raw_input(msg + ' yes/no [no] ')
164 if response == 'y' or response == 'yes':
165 return True
166 elif not response or response == 'n' or response == 'no':
167 return False
168
169
deymo@chromium.org6c039202013-09-12 12:28:12 +0000170def datetime_from_gerrit(date_string):
171 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
172
173
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000174def datetime_from_rietveld(date_string):
deymo@chromium.org29eb6e62014-03-20 01:55:55 +0000175 try:
176 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f')
177 except ValueError:
178 # Sometimes rietveld returns a value without the milliseconds part, so we
179 # attempt to parse those cases as well.
180 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000181
182
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100183def datetime_from_monorail(date_string):
184 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000185
186
187class MyActivity(object):
188 def __init__(self, options):
189 self.options = options
190 self.modified_after = options.begin
191 self.modified_before = options.end
192 self.user = options.user
193 self.changes = []
194 self.reviews = []
195 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100196 self.referenced_issues = []
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000197 self.check_cookies()
198 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-14 19:02:55 -0700199 self.access_errors = set()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000200
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100201 def show_progress(self, how='.'):
202 if sys.stdout.isatty():
203 sys.stdout.write(how)
204 sys.stdout.flush()
205
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000206 # Check the codereview cookie jar to determine which Rietveld instances to
207 # authenticate to.
208 def check_cookies(self):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000209 filtered_instances = []
210
211 def has_cookie(instance):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000212 auth_config = auth.extract_auth_config_from_options(self.options)
213 a = auth.get_authenticator_for_host(instance['url'], auth_config)
214 return a.has_cached_credentials()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000215
216 for instance in rietveld_instances:
217 instance['auth'] = has_cookie(instance)
218
219 if filtered_instances:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000220 logging.warning('No cookie found for the following Rietveld instance%s:',
221 's' if len(filtered_instances) > 1 else '')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000222 for instance in filtered_instances:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000223 logging.warning('\t' + instance['url'])
224 logging.warning('Use --auth if you would like to authenticate to them.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000225
226 def rietveld_search(self, instance, owner=None, reviewer=None):
227 if instance['requires_auth'] and not instance['auth']:
228 return []
229
230
231 email = None if instance['auth'] else ''
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000232 auth_config = auth.extract_auth_config_from_options(self.options)
233 remote = rietveld.Rietveld('https://' + instance['url'], auth_config, email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000234
235 # See def search() in rietveld.py to see all the filters you can use.
236 query_modified_after = None
237
238 if instance['supports_owner_modified_query']:
239 query_modified_after = self.modified_after.strftime('%Y-%m-%d')
240
241 # Rietveld does not allow search by both created_before and modified_after.
242 # (And some instances don't allow search by both owner and modified_after)
243 owner_email = None
244 reviewer_email = None
245 if owner:
246 owner_email = owner + '@' + instance['email_domain']
247 if reviewer:
248 reviewer_email = reviewer + '@' + instance['email_domain']
249 issues = remote.search(
250 owner=owner_email,
251 reviewer=reviewer_email,
252 modified_after=query_modified_after,
253 with_messages=True)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100254 self.show_progress()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000255
256 issues = filter(
257 lambda i: (datetime_from_rietveld(i['created']) < self.modified_before),
258 issues)
259 issues = filter(
260 lambda i: (datetime_from_rietveld(i['modified']) > self.modified_after),
261 issues)
262
263 should_filter_by_user = True
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000264 issues = map(partial(self.process_rietveld_issue, remote, instance), issues)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000265 issues = filter(
266 partial(self.filter_issue, should_filter_by_user=should_filter_by_user),
267 issues)
268 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
269
270 return issues
271
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000272 def extract_bug_numbers_from_description(self, issue):
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000273 description = None
274
275 if 'description' in issue:
276 # Getting the description for Rietveld
277 description = issue['description']
278 elif 'revisions' in issue:
279 # Getting the description for REST Gerrit
280 revision = issue['revisions'][issue['current_revision']]
281 description = revision['commit']['message']
282
283 bugs = []
284 if description:
Nicolas Dossou-gbete903ea732017-07-10 16:46:59 +0100285 # Handle both "Bug: 99999" and "BUG=99999" bug notations
286 # Multiple bugs can be noted on a single line or in multiple ones.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100287 matches = re.findall(
288 r'BUG[=:]\s?((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)', description,
289 flags=re.IGNORECASE)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000290 if matches:
291 for match in matches:
292 bugs.extend(match[0].replace(' ', '').split(','))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100293 # Add default chromium: prefix if none specified.
294 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000295
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000296 return sorted(set(bugs))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000297
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000298 def process_rietveld_issue(self, remote, instance, issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000299 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000300 if self.options.deltas:
301 patchset_props = remote.get_patchset_properties(
302 issue['issue'],
303 issue['patchsets'][-1])
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100304 self.show_progress()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000305 ret['delta'] = '+%d,-%d' % (
306 sum(f['num_added'] for f in patchset_props['files'].itervalues()),
307 sum(f['num_removed'] for f in patchset_props['files'].itervalues()))
308
309 if issue['landed_days_ago'] != 'unknown':
310 ret['status'] = 'committed'
311 elif issue['closed']:
312 ret['status'] = 'closed'
313 elif len(issue['reviewers']) and issue['all_required_reviewers_approved']:
314 ret['status'] = 'ready'
315 else:
316 ret['status'] = 'open'
317
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000318 ret['owner'] = issue['owner_email']
319 ret['author'] = ret['owner']
320
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000321 ret['reviewers'] = set(issue['reviewers'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000322
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000323 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700324 url = instance['shorturl']
325 protocol = instance.get('short_url_protocol', 'http')
326 else:
327 url = instance['url']
328 protocol = 'https'
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000329
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700330 ret['review_url'] = '%s://%s/%d' % (protocol, url, issue['issue'])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000331
332 # Rietveld sometimes has '\r\n' instead of '\n'.
333 ret['header'] = issue['description'].replace('\r', '').split('\n')[0]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000334
335 ret['modified'] = datetime_from_rietveld(issue['modified'])
336 ret['created'] = datetime_from_rietveld(issue['created'])
337 ret['replies'] = self.process_rietveld_replies(issue['messages'])
338
Andrii Shyshkalov024a3312018-06-29 21:59:06 +0000339 ret['bugs'] = self.extract_bug_numbers_from_description(issue)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000340 ret['landed_days_ago'] = issue['landed_days_ago']
341
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000342 return ret
343
344 @staticmethod
345 def process_rietveld_replies(replies):
346 ret = []
347 for reply in replies:
348 r = {}
349 r['author'] = reply['sender']
350 r['created'] = datetime_from_rietveld(reply['date'])
351 r['content'] = ''
352 ret.append(r)
353 return ret
354
Vadim Bendebury8de38002018-05-14 19:02:55 -0700355 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200356 # Convert the "key:value" filter to a list of (key, value) pairs.
357 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000358 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000359 # Instantiate the generator to force all the requests now and catch the
360 # errors here.
361 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000362 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
363 'CURRENT_REVISION', 'CURRENT_COMMIT']))
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000364 except gerrit_util.GerritError, e:
Vadim Bendebury8de38002018-05-14 19:02:55 -0700365 error_message = 'Looking up %r: %s' % (instance['url'], e)
366 if error_message not in self.access_errors:
367 self.access_errors.add(error_message)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000368 return []
369
deymo@chromium.org6c039202013-09-12 12:28:12 +0000370 def gerrit_search(self, instance, owner=None, reviewer=None):
371 max_age = datetime.today() - self.modified_after
Andrii Shyshkalov4aedec02018-09-17 16:31:17 +0000372 filters = ['-age:%ss' % (max_age.days * 24 * 3600 + max_age.seconds)]
373 if owner:
374 assert not reviewer
375 filters.append('owner:%s' % owner)
376 else:
377 filters.extend(('-owner:%s' % reviewer, 'reviewer:%s' % reviewer))
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)
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000527 # TODO(tandrii): support non-chromium email, too.
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000528 user_str = '%s@chromium.org' % self.user
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000529
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100530 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000531 'maxResults': 10000,
532 'q': user_str,
533 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
534 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000535 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000536
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000537 if self.options.completed_issues:
538 return [
539 issue for issue in issues
540 if (self.match(issue['owner']) and
541 issue['status'].lower() in ('verified', 'fixed'))
542 ]
543
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100544 return [
545 issue for issue in issues
546 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000547
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100548 def monorail_get_issues(self, project, issue_ids):
549 return self.monorail_query_issues(project, {
550 'maxResults': 10000,
551 'q': 'id:%s' % ','.join(issue_ids)
552 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000553
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000554 def print_heading(self, heading):
555 print
556 print self.options.output_format_heading.format(heading=heading)
557
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000558 def match(self, author):
559 if '@' in self.user:
560 return author == self.user
561 return author.startswith(self.user + '@')
562
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000563 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000564 activity = len([
565 reply
566 for reply in change['replies']
567 if self.match(reply['author'])
568 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000569 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000570 'created': change['created'].date().isoformat(),
571 'modified': change['modified'].date().isoformat(),
572 'reviewers': ', '.join(change['reviewers']),
573 'status': change['status'],
574 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000575 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000576 if self.options.deltas:
577 optional_values['delta'] = change['delta']
578
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000579 self.print_generic(self.options.output_format,
580 self.options.output_format_changes,
581 change['header'],
582 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000583 change['author'],
584 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000585
586 def print_issue(self, issue):
587 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000588 'created': issue['created'].date().isoformat(),
589 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000590 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000591 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000592 }
593 self.print_generic(self.options.output_format,
594 self.options.output_format_issues,
595 issue['header'],
596 issue['url'],
597 issue['author'],
598 optional_values)
599
600 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000601 activity = len([
602 reply
603 for reply in review['replies']
604 if self.match(reply['author'])
605 ])
606 optional_values = {
607 'created': review['created'].date().isoformat(),
608 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800609 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000610 'activity': activity,
611 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800612 if self.options.deltas:
613 optional_values['delta'] = review['delta']
614
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000615 self.print_generic(self.options.output_format,
616 self.options.output_format_reviews,
617 review['header'],
618 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000619 review['author'],
620 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000621
622 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000623 def print_generic(default_fmt, specific_fmt,
624 title, url, author,
625 optional_values=None):
626 output_format = specific_fmt if specific_fmt is not None else default_fmt
627 output_format = unicode(output_format)
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000628 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000629 'title': title,
630 'url': url,
631 'author': author,
632 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000633 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000634 values.update(optional_values)
635 print DefaultFormatter().format(output_format, **values).encode(
636 sys.getdefaultencoding())
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000637
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000638
639 def filter_issue(self, issue, should_filter_by_user=True):
640 def maybe_filter_username(email):
641 return not should_filter_by_user or username(email) == self.user
642 if (maybe_filter_username(issue['author']) and
643 self.filter_modified(issue['created'])):
644 return True
645 if (maybe_filter_username(issue['owner']) and
646 (self.filter_modified(issue['created']) or
647 self.filter_modified(issue['modified']))):
648 return True
649 for reply in issue['replies']:
650 if self.filter_modified(reply['created']):
651 if not should_filter_by_user:
652 break
653 if (username(reply['author']) == self.user
654 or (self.user + '@') in reply['content']):
655 break
656 else:
657 return False
658 return True
659
660 def filter_modified(self, modified):
661 return self.modified_after < modified and modified < self.modified_before
662
663 def auth_for_changes(self):
664 #TODO(cjhopman): Move authentication check for getting changes here.
665 pass
666
667 def auth_for_reviews(self):
668 # Reviews use all the same instances as changes so no authentication is
669 # required.
670 pass
671
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000672 def get_changes(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100673 num_instances = len(rietveld_instances) + len(gerrit_instances)
674 with contextlib.closing(ThreadPool(num_instances)) as pool:
675 rietveld_changes = pool.map_async(
676 lambda instance: self.rietveld_search(instance, owner=self.user),
677 rietveld_instances)
678 gerrit_changes = pool.map_async(
679 lambda instance: self.gerrit_search(instance, owner=self.user),
680 gerrit_instances)
681 rietveld_changes = itertools.chain.from_iterable(rietveld_changes.get())
682 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
683 self.changes = list(rietveld_changes) + list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000684
685 def print_changes(self):
686 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000687 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000688 for change in self.changes:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100689 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000690
Vadim Bendebury8de38002018-05-14 19:02:55 -0700691 def print_access_errors(self):
692 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400693 logging.error('Access Errors:')
694 for error in self.access_errors:
695 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700696
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000697 def get_reviews(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100698 num_instances = len(rietveld_instances) + len(gerrit_instances)
699 with contextlib.closing(ThreadPool(num_instances)) as pool:
700 rietveld_reviews = pool.map_async(
701 lambda instance: self.rietveld_search(instance, reviewer=self.user),
702 rietveld_instances)
703 gerrit_reviews = pool.map_async(
704 lambda instance: self.gerrit_search(instance, reviewer=self.user),
705 gerrit_instances)
706 rietveld_reviews = itertools.chain.from_iterable(rietveld_reviews.get())
707 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100708 self.reviews = list(rietveld_reviews) + list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000709
710 def print_reviews(self):
711 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000712 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000713 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000714 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000715
716 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100717 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
718 monorail_issues = pool.map(
719 self.monorail_issue_search, monorail_projects.keys())
720 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
721
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700722 if not monorail_issues:
723 return
724
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100725 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
726 filtered_issues = pool.map(
727 self.filter_modified_monorail_issue, monorail_issues)
728 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100729
730 def get_referenced_issues(self):
731 if not self.issues:
732 self.get_issues()
733
734 if not self.changes:
735 self.get_changes()
736
737 referenced_issue_uids = set(itertools.chain.from_iterable(
738 change['bugs'] for change in self.changes))
739 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
740 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
741
742 missing_issues_by_project = collections.defaultdict(list)
743 for issue_uid in missing_issue_uids:
744 project, issue_id = issue_uid.split(':')
745 missing_issues_by_project[project].append(issue_id)
746
747 for project, issue_ids in missing_issues_by_project.iteritems():
748 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000749
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000750 def print_issues(self):
751 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000752 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000753 for issue in self.issues:
754 self.print_issue(issue)
755
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100756 def print_changes_by_issue(self, skip_empty_own):
757 if not self.issues or not self.changes:
758 return
759
760 self.print_heading('Changes by referenced issue(s)')
761 issues = {issue['uid']: issue for issue in self.issues}
762 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
763 changes_by_issue_uid = collections.defaultdict(list)
764 changes_by_ref_issue_uid = collections.defaultdict(list)
765 changes_without_issue = []
766 for change in self.changes:
767 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10 +0000768 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100769 if issue_uid in issues:
770 changes_by_issue_uid[issue_uid].append(change)
771 added = True
772 if issue_uid in ref_issues:
773 changes_by_ref_issue_uid[issue_uid].append(change)
774 added = True
775 if not added:
776 changes_without_issue.append(change)
777
778 # Changes referencing own issues.
779 for issue_uid in issues:
780 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
781 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +0000782 if changes_by_issue_uid[issue_uid]:
783 print
784 for change in changes_by_issue_uid[issue_uid]:
785 print ' ', # this prints one space due to comma, but no newline
786 self.print_change(change)
787 print
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100788
789 # Changes referencing others' issues.
790 for issue_uid in ref_issues:
791 assert changes_by_ref_issue_uid[issue_uid]
792 self.print_issue(ref_issues[issue_uid])
793 for change in changes_by_ref_issue_uid[issue_uid]:
794 print '', # this prints one space due to comma, but no newline
795 self.print_change(change)
796
797 # Changes referencing no issues.
798 if changes_without_issue:
799 print self.options.output_format_no_url.format(title='Other changes')
800 for change in changes_without_issue:
801 print '', # this prints one space due to comma, but no newline
802 self.print_change(change)
803
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000804 def print_activity(self):
805 self.print_changes()
806 self.print_reviews()
807 self.print_issues()
808
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000809 def dump_json(self, ignore_keys=None):
810 if ignore_keys is None:
811 ignore_keys = ['replies']
812
813 def format_for_json_dump(in_array):
814 output = {}
815 for item in in_array:
816 url = item.get('url') or item.get('review_url')
817 if not url:
818 raise Exception('Dumped item %s does not specify url' % item)
819 output[url] = dict(
820 (k, v) for k,v in item.iteritems() if k not in ignore_keys)
821 return output
822
823 class PythonObjectEncoder(json.JSONEncoder):
824 def default(self, obj): # pylint: disable=method-hidden
825 if isinstance(obj, datetime):
826 return obj.isoformat()
827 if isinstance(obj, set):
828 return list(obj)
829 return json.JSONEncoder.default(self, obj)
830
831 output = {
832 'reviews': format_for_json_dump(self.reviews),
833 'changes': format_for_json_dump(self.changes),
834 'issues': format_for_json_dump(self.issues)
835 }
836 print json.dumps(output, indent=2, cls=PythonObjectEncoder)
837
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000838
839def main():
840 # Silence upload.py.
841 rietveld.upload.verbosity = 0
842
843 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
844 parser.add_option(
845 '-u', '--user', metavar='<email>',
846 default=os.environ.get('USER'),
847 help='Filter on user, default=%default')
848 parser.add_option(
849 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000850 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000851 parser.add_option(
852 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000853 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000854 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
855 relativedelta(months=2))
856 parser.add_option(
857 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000858 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000859 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
860 parser.add_option(
861 '-Y', '--this_year', action='store_true',
862 help='Use this year\'s dates')
863 parser.add_option(
864 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000865 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000866 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000867 '-W', '--last_week', action='count',
868 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000869 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000870 '-a', '--auth',
871 action='store_true',
872 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000873 parser.add_option(
874 '-d', '--deltas',
875 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800876 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100877 parser.add_option(
878 '--no-referenced-issues',
879 action='store_true',
880 help='Do not fetch issues referenced by owned changes. Useful in '
881 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100882 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100883 parser.add_option(
884 '--skip-own-issues-without-changes',
885 action='store_true',
886 help='Skips listing own issues without changes when showing changes '
887 'grouped by referenced issue(s). See --changes-by-issue for more '
888 'details.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000889
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000890 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000891 'By default, all activity will be looked up and '
892 'printed. If any of these are specified, only '
893 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000894 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000895 '-c', '--changes',
896 action='store_true',
897 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000898 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000899 '-i', '--issues',
900 action='store_true',
901 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000902 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000903 '-r', '--reviews',
904 action='store_true',
905 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100906 activity_types_group.add_option(
907 '--changes-by-issue', action='store_true',
908 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000909 parser.add_option_group(activity_types_group)
910
911 output_format_group = optparse.OptionGroup(parser, 'Output Format',
912 'By default, all activity will be printed in the '
913 'following format: {url} {title}. This can be '
914 'changed for either all activity types or '
915 'individually for each activity type. The format '
916 'is defined as documented for '
917 'string.format(...). The variables available for '
918 'all activity types are url, title and author. '
919 'Format options for specific activity types will '
920 'override the generic format.')
921 output_format_group.add_option(
922 '-f', '--output-format', metavar='<format>',
923 default=u'{url} {title}',
924 help='Specifies the format to use when printing all your activity.')
925 output_format_group.add_option(
926 '--output-format-changes', metavar='<format>',
927 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000928 help='Specifies the format to use when printing changes. Supports the '
929 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000930 output_format_group.add_option(
931 '--output-format-issues', metavar='<format>',
932 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000933 help='Specifies the format to use when printing issues. Supports the '
934 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000935 output_format_group.add_option(
936 '--output-format-reviews', metavar='<format>',
937 default=None,
938 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000939 output_format_group.add_option(
940 '--output-format-heading', metavar='<format>',
941 default=u'{heading}:',
942 help='Specifies the format to use when printing headings.')
943 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100944 '--output-format-no-url', default='{title}',
945 help='Specifies the format to use when printing activity without url.')
946 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000947 '-m', '--markdown', action='store_true',
948 help='Use markdown-friendly output (overrides --output-format '
949 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000950 output_format_group.add_option(
951 '-j', '--json', action='store_true',
952 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000953 parser.add_option_group(output_format_group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000954 auth.add_auth_options(parser)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000955
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000956 parser.add_option(
957 '-v', '--verbose',
958 action='store_const',
959 dest='verbosity',
960 default=logging.WARN,
961 const=logging.INFO,
962 help='Output extra informational messages.'
963 )
964 parser.add_option(
965 '-q', '--quiet',
966 action='store_const',
967 dest='verbosity',
968 const=logging.ERROR,
969 help='Suppress non-error messages.'
970 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000971 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51 +0000972 '-M', '--merged-only',
973 action='store_true',
974 dest='merged_only',
975 default=False,
976 help='Shows only changes that have been merged.')
977 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41 +0000978 '-C', '--completed-issues',
979 action='store_true',
980 dest='completed_issues',
981 default=False,
982 help='Shows only monorail issues that have completed (Fixed|Verified) '
983 'by the user.')
984 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000985 '-o', '--output', metavar='<file>',
986 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000987
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000988 # Remove description formatting
989 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800990 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000991
992 options, args = parser.parse_args()
993 options.local_user = os.environ.get('USER')
994 if args:
995 parser.error('Args unsupported')
996 if not options.user:
997 parser.error('USER is not set, please use -u')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000998 options.user = username(options.user)
999
Tobias Sargeantffb3c432017-03-08 14:09:14 +00001000 logging.basicConfig(level=options.verbosity)
1001
1002 # python-keyring provides easy access to the system keyring.
1003 try:
1004 import keyring # pylint: disable=unused-import,unused-variable,F0401
1005 except ImportError:
1006 logging.warning('Consider installing python-keyring')
1007
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001008 if not options.begin:
1009 if options.last_quarter:
1010 begin, end = quarter_begin, quarter_end
1011 elif options.this_year:
1012 begin, end = get_year_of(datetime.today())
1013 elif options.week_of:
1014 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +00001015 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +00001016 begin, end = (get_week_of(datetime.today() -
1017 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001018 else:
1019 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
1020 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -07001021 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001022 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -07001023 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001024 else:
1025 end = datetime.today()
1026 options.begin, options.end = begin, end
1027
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +00001028 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:46 +00001029 options.output_format_heading = '### {heading}\n'
1030 options.output_format = ' * [{title}]({url})'
1031 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +00001032 logging.info('Searching for activity by %s', options.user)
1033 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001034
1035 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +01001036 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001037
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001038 if not (options.changes or options.reviews or options.issues or
1039 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001040 options.changes = True
1041 options.issues = True
1042 options.reviews = True
1043
1044 # First do any required authentication so none of the user interaction has to
1045 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001046 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001047 my_activity.auth_for_changes()
1048 if options.reviews:
1049 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001050
Tobias Sargeantffb3c432017-03-08 14:09:14 +00001051 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001052
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001053 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001054 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001055 my_activity.get_changes()
1056 if options.reviews:
1057 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001058 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001059 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001060 if not options.no_referenced_issues:
1061 my_activity.get_referenced_issues()
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001062 except auth.AuthenticationError as e:
Tobias Sargeantffb3c432017-03-08 14:09:14 +00001063 logging.error('auth.AuthenticationError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001064
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +01001065 my_activity.show_progress('\n')
1066
Vadim Bendebury8de38002018-05-14 19:02:55 -07001067 my_activity.print_access_errors()
1068
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +00001069 output_file = None
1070 try:
1071 if options.output:
1072 output_file = open(options.output, 'w')
1073 logging.info('Printing output to "%s"', options.output)
1074 sys.stdout = output_file
1075 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -07001076 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +00001077 else:
1078 if options.json:
1079 my_activity.dump_json()
1080 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001081 if options.changes:
1082 my_activity.print_changes()
1083 if options.reviews:
1084 my_activity.print_reviews()
1085 if options.issues:
1086 my_activity.print_issues()
1087 if options.changes_by_issue:
1088 my_activity.print_changes_by_issue(
1089 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +00001090 finally:
1091 if output_file:
1092 logging.info('Done printing to file.')
1093 sys.stdout = sys.__stdout__
1094 output_file.close()
1095
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001096 return 0
1097
1098
1099if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +00001100 # Fix encoding to support non-ascii issue titles.
1101 fix_encoding.fix_encoding()
1102
sbc@chromium.org013731e2015-02-26 18:28:43 +00001103 try:
1104 sys.exit(main())
1105 except KeyboardInterrupt:
1106 sys.stderr.write('interrupted\n')
1107 sys.exit(1)