blob: 39388a128b458f0a737000649ddcac00f6b8f7d9 [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
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000272 def extract_bug_number_from_description(self, issue):
273 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
296 return bugs
297
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
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100339 ret['bugs'] = self.extract_bug_number_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
372 max_age = max_age.days * 24 * 3600 + max_age.seconds
373 user_filter = 'owner:%s' % owner if owner else 'reviewer:%s' % reviewer
374 filters = ['-age:%ss' % max_age, user_filter]
375
Aaron Gable2979a872017-09-05 17:38:32 -0700376 issues = self.gerrit_changes_over_rest(instance, filters)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100377 self.show_progress()
Aaron Gable2979a872017-09-05 17:38:32 -0700378 issues = [self.process_gerrit_issue(instance, issue)
379 for issue in issues]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000380
381 # TODO(cjhopman): should we filter abandoned changes?
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000382 issues = filter(self.filter_issue, issues)
383 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
384
385 return issues
386
Aaron Gable2979a872017-09-05 17:38:32 -0700387 def process_gerrit_issue(self, instance, issue):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000388 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000389 if self.options.deltas:
390 ret['delta'] = DefaultFormatter().format(
391 '+{insertions},-{deletions}',
392 **issue)
393 ret['status'] = issue['status']
deymo@chromium.org6c039202013-09-12 12:28:12 +0000394 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700395 protocol = instance.get('short_url_protocol', 'http')
396 url = instance['shorturl']
397 else:
398 protocol = 'https'
399 url = instance['url']
400 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
401
deymo@chromium.org6c039202013-09-12 12:28:12 +0000402 ret['header'] = issue['subject']
403 ret['owner'] = issue['owner']['email']
404 ret['author'] = ret['owner']
405 ret['created'] = datetime_from_gerrit(issue['created'])
406 ret['modified'] = datetime_from_gerrit(issue['updated'])
407 if 'messages' in issue:
Aaron Gable2979a872017-09-05 17:38:32 -0700408 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
deymo@chromium.org6c039202013-09-12 12:28:12 +0000409 else:
410 ret['replies'] = []
411 ret['reviewers'] = set(r['author'] for r in ret['replies'])
412 ret['reviewers'].discard(ret['author'])
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100413 ret['bugs'] = self.extract_bug_number_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000414 return ret
415
416 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700417 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000418 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000419 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
420 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000421 for reply in replies:
422 ret.append({
423 'author': reply['author']['email'],
424 'created': datetime_from_gerrit(reply['date']),
425 'content': reply['message'],
426 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000427 return ret
428
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100429 def monorail_get_auth_http(self):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000430 auth_config = auth.extract_auth_config_from_options(self.options)
431 authenticator = auth.get_authenticator_for_host(
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000432 'bugs.chromium.org', auth_config)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100433 return authenticator.authorize(httplib2.Http())
434
435 def filter_modified_monorail_issue(self, issue):
436 """Precisely checks if an issue has been modified in the time range.
437
438 This fetches all issue comments to check if the issue has been modified in
439 the time range specified by user. This is needed because monorail only
440 allows filtering by last updated and published dates, which is not
441 sufficient to tell whether a given issue has been modified at some specific
442 time range. Any update to the issue is a reported as comment on Monorail.
443
444 Args:
445 issue: Issue dict as returned by monorail_query_issues method. In
446 particular, must have a key 'uid' formatted as 'project:issue_id'.
447
448 Returns:
449 Passed issue if modified, None otherwise.
450 """
451 http = self.monorail_get_auth_http()
452 project, issue_id = issue['uid'].split(':')
453 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
454 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
455 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100456 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100457 content = json.loads(body)
458 if not content:
459 logging.error('Unable to parse %s response from monorail.', project)
460 return issue
461
462 for item in content.get('items', []):
463 comment_published = datetime_from_monorail(item['published'])
464 if self.filter_modified(comment_published):
465 return issue
466
467 return None
468
469 def monorail_query_issues(self, project, query):
470 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000471 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100472 '/%s/issues') % project
473 query_data = urllib.urlencode(query)
474 url = url + '?' + query_data
475 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +0100476 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100477 content = json.loads(body)
478 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100479 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100480 return []
481
482 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100483 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100484 for item in content.get('items', []):
485 if project_config.get('shorturl'):
486 protocol = project_config.get('short_url_protocol', 'http')
487 item_url = '%s://%s/%d' % (
488 protocol, project_config['shorturl'], item['id'])
489 else:
490 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
491 project, item['id'])
492 issue = {
493 'uid': '%s:%s' % (project, item['id']),
494 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100495 'created': datetime_from_monorail(item['published']),
496 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100497 'author': item['author']['name'],
498 'url': item_url,
499 'comments': [],
500 'status': item['status'],
501 'labels': [],
502 'components': []
503 }
504 if 'owner' in item:
505 issue['owner'] = item['owner']['name']
506 else:
507 issue['owner'] = 'None'
508 if 'labels' in item:
509 issue['labels'] = item['labels']
510 if 'components' in item:
511 issue['components'] = item['components']
512 issues.append(issue)
513
514 return issues
515
516 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000517 epoch = datetime.utcfromtimestamp(0)
518 user_str = '%s@chromium.org' % self.user
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000519
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100520 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000521 'maxResults': 10000,
522 'q': user_str,
523 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
524 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000525 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000526
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100527 return [
528 issue for issue in issues
529 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000530
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100531 def monorail_get_issues(self, project, issue_ids):
532 return self.monorail_query_issues(project, {
533 'maxResults': 10000,
534 'q': 'id:%s' % ','.join(issue_ids)
535 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000536
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000537 def print_heading(self, heading):
538 print
539 print self.options.output_format_heading.format(heading=heading)
540
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000541 def match(self, author):
542 if '@' in self.user:
543 return author == self.user
544 return author.startswith(self.user + '@')
545
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000546 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000547 activity = len([
548 reply
549 for reply in change['replies']
550 if self.match(reply['author'])
551 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000552 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000553 'created': change['created'].date().isoformat(),
554 'modified': change['modified'].date().isoformat(),
555 'reviewers': ', '.join(change['reviewers']),
556 'status': change['status'],
557 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000558 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000559 if self.options.deltas:
560 optional_values['delta'] = change['delta']
561
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000562 self.print_generic(self.options.output_format,
563 self.options.output_format_changes,
564 change['header'],
565 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000566 change['author'],
567 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000568
569 def print_issue(self, issue):
570 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000571 'created': issue['created'].date().isoformat(),
572 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000573 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000574 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000575 }
576 self.print_generic(self.options.output_format,
577 self.options.output_format_issues,
578 issue['header'],
579 issue['url'],
580 issue['author'],
581 optional_values)
582
583 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000584 activity = len([
585 reply
586 for reply in review['replies']
587 if self.match(reply['author'])
588 ])
589 optional_values = {
590 'created': review['created'].date().isoformat(),
591 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800592 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000593 'activity': activity,
594 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800595 if self.options.deltas:
596 optional_values['delta'] = review['delta']
597
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000598 self.print_generic(self.options.output_format,
599 self.options.output_format_reviews,
600 review['header'],
601 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000602 review['author'],
603 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000604
605 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000606 def print_generic(default_fmt, specific_fmt,
607 title, url, author,
608 optional_values=None):
609 output_format = specific_fmt if specific_fmt is not None else default_fmt
610 output_format = unicode(output_format)
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000611 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000612 'title': title,
613 'url': url,
614 'author': author,
615 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000616 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000617 values.update(optional_values)
618 print DefaultFormatter().format(output_format, **values).encode(
619 sys.getdefaultencoding())
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000620
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000621
622 def filter_issue(self, issue, should_filter_by_user=True):
623 def maybe_filter_username(email):
624 return not should_filter_by_user or username(email) == self.user
625 if (maybe_filter_username(issue['author']) and
626 self.filter_modified(issue['created'])):
627 return True
628 if (maybe_filter_username(issue['owner']) and
629 (self.filter_modified(issue['created']) or
630 self.filter_modified(issue['modified']))):
631 return True
632 for reply in issue['replies']:
633 if self.filter_modified(reply['created']):
634 if not should_filter_by_user:
635 break
636 if (username(reply['author']) == self.user
637 or (self.user + '@') in reply['content']):
638 break
639 else:
640 return False
641 return True
642
643 def filter_modified(self, modified):
644 return self.modified_after < modified and modified < self.modified_before
645
646 def auth_for_changes(self):
647 #TODO(cjhopman): Move authentication check for getting changes here.
648 pass
649
650 def auth_for_reviews(self):
651 # Reviews use all the same instances as changes so no authentication is
652 # required.
653 pass
654
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000655 def get_changes(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100656 num_instances = len(rietveld_instances) + len(gerrit_instances)
657 with contextlib.closing(ThreadPool(num_instances)) as pool:
658 rietveld_changes = pool.map_async(
659 lambda instance: self.rietveld_search(instance, owner=self.user),
660 rietveld_instances)
661 gerrit_changes = pool.map_async(
662 lambda instance: self.gerrit_search(instance, owner=self.user),
663 gerrit_instances)
664 rietveld_changes = itertools.chain.from_iterable(rietveld_changes.get())
665 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
666 self.changes = list(rietveld_changes) + list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000667
668 def print_changes(self):
669 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000670 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000671 for change in self.changes:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100672 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000673
Vadim Bendebury8de38002018-05-14 19:02:55 -0700674 def print_access_errors(self):
675 if self.access_errors:
Ryan Harrison398fb442018-05-22 12:05:26 -0400676 logging.error('Access Errors:')
677 for error in self.access_errors:
678 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-14 19:02:55 -0700679
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000680 def get_reviews(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100681 num_instances = len(rietveld_instances) + len(gerrit_instances)
682 with contextlib.closing(ThreadPool(num_instances)) as pool:
683 rietveld_reviews = pool.map_async(
684 lambda instance: self.rietveld_search(instance, reviewer=self.user),
685 rietveld_instances)
686 gerrit_reviews = pool.map_async(
687 lambda instance: self.gerrit_search(instance, reviewer=self.user),
688 gerrit_instances)
689 rietveld_reviews = itertools.chain.from_iterable(rietveld_reviews.get())
690 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
691 gerrit_reviews = [r for r in gerrit_reviews if r['owner'] != self.user]
692 self.reviews = list(rietveld_reviews) + list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000693
694 def print_reviews(self):
695 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000696 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000697 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000698 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000699
700 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100701 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
702 monorail_issues = pool.map(
703 self.monorail_issue_search, monorail_projects.keys())
704 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
705
Vadim Bendeburycbf02042018-05-14 17:46:15 -0700706 if not monorail_issues:
707 return
708
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100709 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
710 filtered_issues = pool.map(
711 self.filter_modified_monorail_issue, monorail_issues)
712 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100713
714 def get_referenced_issues(self):
715 if not self.issues:
716 self.get_issues()
717
718 if not self.changes:
719 self.get_changes()
720
721 referenced_issue_uids = set(itertools.chain.from_iterable(
722 change['bugs'] for change in self.changes))
723 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
724 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
725
726 missing_issues_by_project = collections.defaultdict(list)
727 for issue_uid in missing_issue_uids:
728 project, issue_id = issue_uid.split(':')
729 missing_issues_by_project[project].append(issue_id)
730
731 for project, issue_ids in missing_issues_by_project.iteritems():
732 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000733
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000734 def print_issues(self):
735 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000736 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000737 for issue in self.issues:
738 self.print_issue(issue)
739
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100740 def print_changes_by_issue(self, skip_empty_own):
741 if not self.issues or not self.changes:
742 return
743
744 self.print_heading('Changes by referenced issue(s)')
745 issues = {issue['uid']: issue for issue in self.issues}
746 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
747 changes_by_issue_uid = collections.defaultdict(list)
748 changes_by_ref_issue_uid = collections.defaultdict(list)
749 changes_without_issue = []
750 for change in self.changes:
751 added = False
752 for issue_uid in change['bugs']:
753 if issue_uid in issues:
754 changes_by_issue_uid[issue_uid].append(change)
755 added = True
756 if issue_uid in ref_issues:
757 changes_by_ref_issue_uid[issue_uid].append(change)
758 added = True
759 if not added:
760 changes_without_issue.append(change)
761
762 # Changes referencing own issues.
763 for issue_uid in issues:
764 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
765 self.print_issue(issues[issue_uid])
766 for change in changes_by_issue_uid[issue_uid]:
767 print '', # this prints one space due to comma, but no newline
768 self.print_change(change)
769
770 # Changes referencing others' issues.
771 for issue_uid in ref_issues:
772 assert changes_by_ref_issue_uid[issue_uid]
773 self.print_issue(ref_issues[issue_uid])
774 for change in changes_by_ref_issue_uid[issue_uid]:
775 print '', # this prints one space due to comma, but no newline
776 self.print_change(change)
777
778 # Changes referencing no issues.
779 if changes_without_issue:
780 print self.options.output_format_no_url.format(title='Other changes')
781 for change in changes_without_issue:
782 print '', # this prints one space due to comma, but no newline
783 self.print_change(change)
784
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000785 def print_activity(self):
786 self.print_changes()
787 self.print_reviews()
788 self.print_issues()
789
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000790 def dump_json(self, ignore_keys=None):
791 if ignore_keys is None:
792 ignore_keys = ['replies']
793
794 def format_for_json_dump(in_array):
795 output = {}
796 for item in in_array:
797 url = item.get('url') or item.get('review_url')
798 if not url:
799 raise Exception('Dumped item %s does not specify url' % item)
800 output[url] = dict(
801 (k, v) for k,v in item.iteritems() if k not in ignore_keys)
802 return output
803
804 class PythonObjectEncoder(json.JSONEncoder):
805 def default(self, obj): # pylint: disable=method-hidden
806 if isinstance(obj, datetime):
807 return obj.isoformat()
808 if isinstance(obj, set):
809 return list(obj)
810 return json.JSONEncoder.default(self, obj)
811
812 output = {
813 'reviews': format_for_json_dump(self.reviews),
814 'changes': format_for_json_dump(self.changes),
815 'issues': format_for_json_dump(self.issues)
816 }
817 print json.dumps(output, indent=2, cls=PythonObjectEncoder)
818
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000819
820def main():
821 # Silence upload.py.
822 rietveld.upload.verbosity = 0
823
824 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
825 parser.add_option(
826 '-u', '--user', metavar='<email>',
827 default=os.environ.get('USER'),
828 help='Filter on user, default=%default')
829 parser.add_option(
830 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000831 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000832 parser.add_option(
833 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000834 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000835 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
836 relativedelta(months=2))
837 parser.add_option(
838 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000839 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000840 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
841 parser.add_option(
842 '-Y', '--this_year', action='store_true',
843 help='Use this year\'s dates')
844 parser.add_option(
845 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000846 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000847 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000848 '-W', '--last_week', action='count',
849 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000850 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000851 '-a', '--auth',
852 action='store_true',
853 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000854 parser.add_option(
855 '-d', '--deltas',
856 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800857 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100858 parser.add_option(
859 '--no-referenced-issues',
860 action='store_true',
861 help='Do not fetch issues referenced by owned changes. Useful in '
862 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 17:49:34 +0100863 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100864 parser.add_option(
865 '--skip-own-issues-without-changes',
866 action='store_true',
867 help='Skips listing own issues without changes when showing changes '
868 'grouped by referenced issue(s). See --changes-by-issue for more '
869 'details.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000870
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000871 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000872 'By default, all activity will be looked up and '
873 'printed. If any of these are specified, only '
874 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000875 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000876 '-c', '--changes',
877 action='store_true',
878 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000879 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000880 '-i', '--issues',
881 action='store_true',
882 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000883 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000884 '-r', '--reviews',
885 action='store_true',
886 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100887 activity_types_group.add_option(
888 '--changes-by-issue', action='store_true',
889 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000890 parser.add_option_group(activity_types_group)
891
892 output_format_group = optparse.OptionGroup(parser, 'Output Format',
893 'By default, all activity will be printed in the '
894 'following format: {url} {title}. This can be '
895 'changed for either all activity types or '
896 'individually for each activity type. The format '
897 'is defined as documented for '
898 'string.format(...). The variables available for '
899 'all activity types are url, title and author. '
900 'Format options for specific activity types will '
901 'override the generic format.')
902 output_format_group.add_option(
903 '-f', '--output-format', metavar='<format>',
904 default=u'{url} {title}',
905 help='Specifies the format to use when printing all your activity.')
906 output_format_group.add_option(
907 '--output-format-changes', metavar='<format>',
908 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000909 help='Specifies the format to use when printing changes. Supports the '
910 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000911 output_format_group.add_option(
912 '--output-format-issues', metavar='<format>',
913 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000914 help='Specifies the format to use when printing issues. Supports the '
915 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000916 output_format_group.add_option(
917 '--output-format-reviews', metavar='<format>',
918 default=None,
919 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000920 output_format_group.add_option(
921 '--output-format-heading', metavar='<format>',
922 default=u'{heading}:',
923 help='Specifies the format to use when printing headings.')
924 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100925 '--output-format-no-url', default='{title}',
926 help='Specifies the format to use when printing activity without url.')
927 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000928 '-m', '--markdown', action='store_true',
929 help='Use markdown-friendly output (overrides --output-format '
930 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000931 output_format_group.add_option(
932 '-j', '--json', action='store_true',
933 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000934 parser.add_option_group(output_format_group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000935 auth.add_auth_options(parser)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000936
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000937 parser.add_option(
938 '-v', '--verbose',
939 action='store_const',
940 dest='verbosity',
941 default=logging.WARN,
942 const=logging.INFO,
943 help='Output extra informational messages.'
944 )
945 parser.add_option(
946 '-q', '--quiet',
947 action='store_const',
948 dest='verbosity',
949 const=logging.ERROR,
950 help='Suppress non-error messages.'
951 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000952 parser.add_option(
953 '-o', '--output', metavar='<file>',
954 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000955
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000956 # Remove description formatting
957 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800958 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000959
960 options, args = parser.parse_args()
961 options.local_user = os.environ.get('USER')
962 if args:
963 parser.error('Args unsupported')
964 if not options.user:
965 parser.error('USER is not set, please use -u')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000966 options.user = username(options.user)
967
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000968 logging.basicConfig(level=options.verbosity)
969
970 # python-keyring provides easy access to the system keyring.
971 try:
972 import keyring # pylint: disable=unused-import,unused-variable,F0401
973 except ImportError:
974 logging.warning('Consider installing python-keyring')
975
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000976 if not options.begin:
977 if options.last_quarter:
978 begin, end = quarter_begin, quarter_end
979 elif options.this_year:
980 begin, end = get_year_of(datetime.today())
981 elif options.week_of:
982 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000983 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000984 begin, end = (get_week_of(datetime.today() -
985 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000986 else:
987 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
988 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700989 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000990 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700991 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000992 else:
993 end = datetime.today()
994 options.begin, options.end = begin, end
995
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000996 if options.markdown:
997 options.output_format = ' * [{title}]({url})'
998 options.output_format_heading = '### {heading} ###'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100999 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +00001000 logging.info('Searching for activity by %s', options.user)
1001 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001002
1003 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +01001004 my_activity.show_progress('Loading data')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001005
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001006 if not (options.changes or options.reviews or options.issues or
1007 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001008 options.changes = True
1009 options.issues = True
1010 options.reviews = True
1011
1012 # First do any required authentication so none of the user interaction has to
1013 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001014 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001015 my_activity.auth_for_changes()
1016 if options.reviews:
1017 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001018
Tobias Sargeantffb3c432017-03-08 14:09:14 +00001019 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001020
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001021 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001022 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001023 my_activity.get_changes()
1024 if options.reviews:
1025 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001026 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001027 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001028 if not options.no_referenced_issues:
1029 my_activity.get_referenced_issues()
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001030 except auth.AuthenticationError as e:
Tobias Sargeantffb3c432017-03-08 14:09:14 +00001031 logging.error('auth.AuthenticationError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001032
Sergiy Byelozyorova68d82c2018-03-21 17:20:56 +01001033 my_activity.show_progress('\n')
1034
Vadim Bendebury8de38002018-05-14 19:02:55 -07001035 my_activity.print_access_errors()
1036
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +00001037 output_file = None
1038 try:
1039 if options.output:
1040 output_file = open(options.output, 'w')
1041 logging.info('Printing output to "%s"', options.output)
1042 sys.stdout = output_file
1043 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -07001044 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +00001045 else:
1046 if options.json:
1047 my_activity.dump_json()
1048 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001049 if options.changes:
1050 my_activity.print_changes()
1051 if options.reviews:
1052 my_activity.print_reviews()
1053 if options.issues:
1054 my_activity.print_issues()
1055 if options.changes_by_issue:
1056 my_activity.print_changes_by_issue(
1057 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +00001058 finally:
1059 if output_file:
1060 logging.info('Done printing to file.')
1061 sys.stdout = sys.__stdout__
1062 output_file.close()
1063
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001064 return 0
1065
1066
1067if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +00001068 # Fix encoding to support non-ascii issue titles.
1069 fix_encoding.fix_encoding()
1070
sbc@chromium.org013731e2015-02-26 18:28:43 +00001071 try:
1072 sys.exit(main())
1073 except KeyboardInterrupt:
1074 sys.stderr.write('interrupted\n')
1075 sys.exit(1)