blob: 239f5b8e674f9d7b45e52f29cd4a5ea4a8703717 [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
199
200 # Check the codereview cookie jar to determine which Rietveld instances to
201 # authenticate to.
202 def check_cookies(self):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000203 filtered_instances = []
204
205 def has_cookie(instance):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000206 auth_config = auth.extract_auth_config_from_options(self.options)
207 a = auth.get_authenticator_for_host(instance['url'], auth_config)
208 return a.has_cached_credentials()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000209
210 for instance in rietveld_instances:
211 instance['auth'] = has_cookie(instance)
212
213 if filtered_instances:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000214 logging.warning('No cookie found for the following Rietveld instance%s:',
215 's' if len(filtered_instances) > 1 else '')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000216 for instance in filtered_instances:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000217 logging.warning('\t' + instance['url'])
218 logging.warning('Use --auth if you would like to authenticate to them.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000219
220 def rietveld_search(self, instance, owner=None, reviewer=None):
221 if instance['requires_auth'] and not instance['auth']:
222 return []
223
224
225 email = None if instance['auth'] else ''
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000226 auth_config = auth.extract_auth_config_from_options(self.options)
227 remote = rietveld.Rietveld('https://' + instance['url'], auth_config, email)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000228
229 # See def search() in rietveld.py to see all the filters you can use.
230 query_modified_after = None
231
232 if instance['supports_owner_modified_query']:
233 query_modified_after = self.modified_after.strftime('%Y-%m-%d')
234
235 # Rietveld does not allow search by both created_before and modified_after.
236 # (And some instances don't allow search by both owner and modified_after)
237 owner_email = None
238 reviewer_email = None
239 if owner:
240 owner_email = owner + '@' + instance['email_domain']
241 if reviewer:
242 reviewer_email = reviewer + '@' + instance['email_domain']
243 issues = remote.search(
244 owner=owner_email,
245 reviewer=reviewer_email,
246 modified_after=query_modified_after,
247 with_messages=True)
248
249 issues = filter(
250 lambda i: (datetime_from_rietveld(i['created']) < self.modified_before),
251 issues)
252 issues = filter(
253 lambda i: (datetime_from_rietveld(i['modified']) > self.modified_after),
254 issues)
255
256 should_filter_by_user = True
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000257 issues = map(partial(self.process_rietveld_issue, remote, instance), issues)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000258 issues = filter(
259 partial(self.filter_issue, should_filter_by_user=should_filter_by_user),
260 issues)
261 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
262
263 return issues
264
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000265 def extract_bug_number_from_description(self, issue):
266 description = None
267
268 if 'description' in issue:
269 # Getting the description for Rietveld
270 description = issue['description']
271 elif 'revisions' in issue:
272 # Getting the description for REST Gerrit
273 revision = issue['revisions'][issue['current_revision']]
274 description = revision['commit']['message']
275
276 bugs = []
277 if description:
Nicolas Dossou-gbete903ea732017-07-10 16:46:59 +0100278 # Handle both "Bug: 99999" and "BUG=99999" bug notations
279 # Multiple bugs can be noted on a single line or in multiple ones.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100280 matches = re.findall(
281 r'BUG[=:]\s?((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)', description,
282 flags=re.IGNORECASE)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000283 if matches:
284 for match in matches:
285 bugs.extend(match[0].replace(' ', '').split(','))
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100286 # Add default chromium: prefix if none specified.
287 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000288
289 return bugs
290
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000291 def process_rietveld_issue(self, remote, instance, issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000292 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000293 if self.options.deltas:
294 patchset_props = remote.get_patchset_properties(
295 issue['issue'],
296 issue['patchsets'][-1])
297 ret['delta'] = '+%d,-%d' % (
298 sum(f['num_added'] for f in patchset_props['files'].itervalues()),
299 sum(f['num_removed'] for f in patchset_props['files'].itervalues()))
300
301 if issue['landed_days_ago'] != 'unknown':
302 ret['status'] = 'committed'
303 elif issue['closed']:
304 ret['status'] = 'closed'
305 elif len(issue['reviewers']) and issue['all_required_reviewers_approved']:
306 ret['status'] = 'ready'
307 else:
308 ret['status'] = 'open'
309
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000310 ret['owner'] = issue['owner_email']
311 ret['author'] = ret['owner']
312
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000313 ret['reviewers'] = set(issue['reviewers'])
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000314
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000315 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700316 url = instance['shorturl']
317 protocol = instance.get('short_url_protocol', 'http')
318 else:
319 url = instance['url']
320 protocol = 'https'
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000321
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700322 ret['review_url'] = '%s://%s/%d' % (protocol, url, issue['issue'])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000323
324 # Rietveld sometimes has '\r\n' instead of '\n'.
325 ret['header'] = issue['description'].replace('\r', '').split('\n')[0]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000326
327 ret['modified'] = datetime_from_rietveld(issue['modified'])
328 ret['created'] = datetime_from_rietveld(issue['created'])
329 ret['replies'] = self.process_rietveld_replies(issue['messages'])
330
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100331 ret['bugs'] = self.extract_bug_number_from_description(issue)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000332 ret['landed_days_ago'] = issue['landed_days_ago']
333
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000334 return ret
335
336 @staticmethod
337 def process_rietveld_replies(replies):
338 ret = []
339 for reply in replies:
340 r = {}
341 r['author'] = reply['sender']
342 r['created'] = datetime_from_rietveld(reply['date'])
343 r['content'] = ''
344 ret.append(r)
345 return ret
346
deymo@chromium.org6c039202013-09-12 12:28:12 +0000347 @staticmethod
deymo@chromium.org6c039202013-09-12 12:28:12 +0000348 def gerrit_changes_over_rest(instance, filters):
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200349 # Convert the "key:value" filter to a list of (key, value) pairs.
350 req = list(f.split(':', 1) for f in filters)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000351 try:
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000352 # Instantiate the generator to force all the requests now and catch the
353 # errors here.
354 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000355 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
356 'CURRENT_REVISION', 'CURRENT_COMMIT']))
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000357 except gerrit_util.GerritError, e:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000358 logging.error('Looking up %r: %s', instance['url'], e)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000359 return []
360
deymo@chromium.org6c039202013-09-12 12:28:12 +0000361 def gerrit_search(self, instance, owner=None, reviewer=None):
362 max_age = datetime.today() - self.modified_after
363 max_age = max_age.days * 24 * 3600 + max_age.seconds
364 user_filter = 'owner:%s' % owner if owner else 'reviewer:%s' % reviewer
365 filters = ['-age:%ss' % max_age, user_filter]
366
Aaron Gable2979a872017-09-05 17:38:32 -0700367 issues = self.gerrit_changes_over_rest(instance, filters)
368 issues = [self.process_gerrit_issue(instance, issue)
369 for issue in issues]
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000370
371 # TODO(cjhopman): should we filter abandoned changes?
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000372 issues = filter(self.filter_issue, issues)
373 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
374
375 return issues
376
Aaron Gable2979a872017-09-05 17:38:32 -0700377 def process_gerrit_issue(self, instance, issue):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000378 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000379 if self.options.deltas:
380 ret['delta'] = DefaultFormatter().format(
381 '+{insertions},-{deletions}',
382 **issue)
383 ret['status'] = issue['status']
deymo@chromium.org6c039202013-09-12 12:28:12 +0000384 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 12:55:01 -0700385 protocol = instance.get('short_url_protocol', 'http')
386 url = instance['shorturl']
387 else:
388 protocol = 'https'
389 url = instance['url']
390 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
391
deymo@chromium.org6c039202013-09-12 12:28:12 +0000392 ret['header'] = issue['subject']
393 ret['owner'] = issue['owner']['email']
394 ret['author'] = ret['owner']
395 ret['created'] = datetime_from_gerrit(issue['created'])
396 ret['modified'] = datetime_from_gerrit(issue['updated'])
397 if 'messages' in issue:
Aaron Gable2979a872017-09-05 17:38:32 -0700398 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
deymo@chromium.org6c039202013-09-12 12:28:12 +0000399 else:
400 ret['replies'] = []
401 ret['reviewers'] = set(r['author'] for r in ret['replies'])
402 ret['reviewers'].discard(ret['author'])
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100403 ret['bugs'] = self.extract_bug_number_from_description(issue)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000404 return ret
405
406 @staticmethod
Aaron Gable2979a872017-09-05 17:38:32 -0700407 def process_gerrit_issue_replies(replies):
deymo@chromium.org6c039202013-09-12 12:28:12 +0000408 ret = []
deymo@chromium.orgf8be2762013-11-06 01:01:59 +0000409 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
410 replies)
deymo@chromium.org6c039202013-09-12 12:28:12 +0000411 for reply in replies:
412 ret.append({
413 'author': reply['author']['email'],
414 'created': datetime_from_gerrit(reply['date']),
415 'content': reply['message'],
416 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000417 return ret
418
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100419 def monorail_get_auth_http(self):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000420 auth_config = auth.extract_auth_config_from_options(self.options)
421 authenticator = auth.get_authenticator_for_host(
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000422 'bugs.chromium.org', auth_config)
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100423 return authenticator.authorize(httplib2.Http())
424
425 def filter_modified_monorail_issue(self, issue):
426 """Precisely checks if an issue has been modified in the time range.
427
428 This fetches all issue comments to check if the issue has been modified in
429 the time range specified by user. This is needed because monorail only
430 allows filtering by last updated and published dates, which is not
431 sufficient to tell whether a given issue has been modified at some specific
432 time range. Any update to the issue is a reported as comment on Monorail.
433
434 Args:
435 issue: Issue dict as returned by monorail_query_issues method. In
436 particular, must have a key 'uid' formatted as 'project:issue_id'.
437
438 Returns:
439 Passed issue if modified, None otherwise.
440 """
441 http = self.monorail_get_auth_http()
442 project, issue_id = issue['uid'].split(':')
443 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
444 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
445 _, body = http.request(url)
446 content = json.loads(body)
447 if not content:
448 logging.error('Unable to parse %s response from monorail.', project)
449 return issue
450
451 for item in content.get('items', []):
452 comment_published = datetime_from_monorail(item['published'])
453 if self.filter_modified(comment_published):
454 return issue
455
456 return None
457
458 def monorail_query_issues(self, project, query):
459 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000460 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100461 '/%s/issues') % project
462 query_data = urllib.urlencode(query)
463 url = url + '?' + query_data
464 _, body = http.request(url)
465 content = json.loads(body)
466 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100467 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100468 return []
469
470 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100471 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100472 for item in content.get('items', []):
473 if project_config.get('shorturl'):
474 protocol = project_config.get('short_url_protocol', 'http')
475 item_url = '%s://%s/%d' % (
476 protocol, project_config['shorturl'], item['id'])
477 else:
478 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
479 project, item['id'])
480 issue = {
481 'uid': '%s:%s' % (project, item['id']),
482 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100483 'created': datetime_from_monorail(item['published']),
484 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100485 'author': item['author']['name'],
486 'url': item_url,
487 'comments': [],
488 'status': item['status'],
489 'labels': [],
490 'components': []
491 }
492 if 'owner' in item:
493 issue['owner'] = item['owner']['name']
494 else:
495 issue['owner'] = 'None'
496 if 'labels' in item:
497 issue['labels'] = item['labels']
498 if 'components' in item:
499 issue['components'] = item['components']
500 issues.append(issue)
501
502 return issues
503
504 def monorail_issue_search(self, project):
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000505 epoch = datetime.utcfromtimestamp(0)
506 user_str = '%s@chromium.org' % self.user
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000507
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100508 issues = self.monorail_query_issues(project, {
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000509 'maxResults': 10000,
510 'q': user_str,
511 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
512 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000513 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000514
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100515 return [
516 issue for issue in issues
517 if issue['author'] == user_str or issue['owner'] == user_str]
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000518
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100519 def monorail_get_issues(self, project, issue_ids):
520 return self.monorail_query_issues(project, {
521 'maxResults': 10000,
522 'q': 'id:%s' % ','.join(issue_ids)
523 })
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000524
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000525 def print_heading(self, heading):
526 print
527 print self.options.output_format_heading.format(heading=heading)
528
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000529 def match(self, author):
530 if '@' in self.user:
531 return author == self.user
532 return author.startswith(self.user + '@')
533
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000534 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000535 activity = len([
536 reply
537 for reply in change['replies']
538 if self.match(reply['author'])
539 ])
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000540 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000541 'created': change['created'].date().isoformat(),
542 'modified': change['modified'].date().isoformat(),
543 'reviewers': ', '.join(change['reviewers']),
544 'status': change['status'],
545 'activity': activity,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000546 }
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000547 if self.options.deltas:
548 optional_values['delta'] = change['delta']
549
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000550 self.print_generic(self.options.output_format,
551 self.options.output_format_changes,
552 change['header'],
553 change['review_url'],
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000554 change['author'],
555 optional_values)
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000556
557 def print_issue(self, issue):
558 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000559 'created': issue['created'].date().isoformat(),
560 'modified': issue['modified'].date().isoformat(),
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000561 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000562 'status': issue['status'],
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000563 }
564 self.print_generic(self.options.output_format,
565 self.options.output_format_issues,
566 issue['header'],
567 issue['url'],
568 issue['author'],
569 optional_values)
570
571 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000572 activity = len([
573 reply
574 for reply in review['replies']
575 if self.match(reply['author'])
576 ])
577 optional_values = {
578 'created': review['created'].date().isoformat(),
579 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800580 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000581 'activity': activity,
582 }
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800583 if self.options.deltas:
584 optional_values['delta'] = review['delta']
585
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000586 self.print_generic(self.options.output_format,
587 self.options.output_format_reviews,
588 review['header'],
589 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000590 review['author'],
591 optional_values)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000592
593 @staticmethod
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000594 def print_generic(default_fmt, specific_fmt,
595 title, url, author,
596 optional_values=None):
597 output_format = specific_fmt if specific_fmt is not None else default_fmt
598 output_format = unicode(output_format)
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000599 values = {
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000600 'title': title,
601 'url': url,
602 'author': author,
603 }
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000604 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000605 values.update(optional_values)
606 print DefaultFormatter().format(output_format, **values).encode(
607 sys.getdefaultencoding())
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000608
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000609
610 def filter_issue(self, issue, should_filter_by_user=True):
611 def maybe_filter_username(email):
612 return not should_filter_by_user or username(email) == self.user
613 if (maybe_filter_username(issue['author']) and
614 self.filter_modified(issue['created'])):
615 return True
616 if (maybe_filter_username(issue['owner']) and
617 (self.filter_modified(issue['created']) or
618 self.filter_modified(issue['modified']))):
619 return True
620 for reply in issue['replies']:
621 if self.filter_modified(reply['created']):
622 if not should_filter_by_user:
623 break
624 if (username(reply['author']) == self.user
625 or (self.user + '@') in reply['content']):
626 break
627 else:
628 return False
629 return True
630
631 def filter_modified(self, modified):
632 return self.modified_after < modified and modified < self.modified_before
633
634 def auth_for_changes(self):
635 #TODO(cjhopman): Move authentication check for getting changes here.
636 pass
637
638 def auth_for_reviews(self):
639 # Reviews use all the same instances as changes so no authentication is
640 # required.
641 pass
642
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000643 def get_changes(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100644 num_instances = len(rietveld_instances) + len(gerrit_instances)
645 with contextlib.closing(ThreadPool(num_instances)) as pool:
646 rietveld_changes = pool.map_async(
647 lambda instance: self.rietveld_search(instance, owner=self.user),
648 rietveld_instances)
649 gerrit_changes = pool.map_async(
650 lambda instance: self.gerrit_search(instance, owner=self.user),
651 gerrit_instances)
652 rietveld_changes = itertools.chain.from_iterable(rietveld_changes.get())
653 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
654 self.changes = list(rietveld_changes) + list(gerrit_changes)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000655
656 def print_changes(self):
657 if self.changes:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000658 self.print_heading('Changes')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000659 for change in self.changes:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100660 self.print_change(change)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000661
662 def get_reviews(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100663 num_instances = len(rietveld_instances) + len(gerrit_instances)
664 with contextlib.closing(ThreadPool(num_instances)) as pool:
665 rietveld_reviews = pool.map_async(
666 lambda instance: self.rietveld_search(instance, reviewer=self.user),
667 rietveld_instances)
668 gerrit_reviews = pool.map_async(
669 lambda instance: self.gerrit_search(instance, reviewer=self.user),
670 gerrit_instances)
671 rietveld_reviews = itertools.chain.from_iterable(rietveld_reviews.get())
672 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
673 gerrit_reviews = [r for r in gerrit_reviews if r['owner'] != self.user]
674 self.reviews = list(rietveld_reviews) + list(gerrit_reviews)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000675
676 def print_reviews(self):
677 if self.reviews:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000678 self.print_heading('Reviews')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000679 for review in self.reviews:
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000680 self.print_review(review)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000681
682 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 17:07:28 +0100683 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
684 monorail_issues = pool.map(
685 self.monorail_issue_search, monorail_projects.keys())
686 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
687
688 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
689 filtered_issues = pool.map(
690 self.filter_modified_monorail_issue, monorail_issues)
691 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100692
693 def get_referenced_issues(self):
694 if not self.issues:
695 self.get_issues()
696
697 if not self.changes:
698 self.get_changes()
699
700 referenced_issue_uids = set(itertools.chain.from_iterable(
701 change['bugs'] for change in self.changes))
702 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
703 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
704
705 missing_issues_by_project = collections.defaultdict(list)
706 for issue_uid in missing_issue_uids:
707 project, issue_id = issue_uid.split(':')
708 missing_issues_by_project[project].append(issue_id)
709
710 for project, issue_ids in missing_issues_by_project.iteritems():
711 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000712
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000713 def print_issues(self):
714 if self.issues:
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000715 self.print_heading('Issues')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000716 for issue in self.issues:
717 self.print_issue(issue)
718
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100719 def print_changes_by_issue(self, skip_empty_own):
720 if not self.issues or not self.changes:
721 return
722
723 self.print_heading('Changes by referenced issue(s)')
724 issues = {issue['uid']: issue for issue in self.issues}
725 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
726 changes_by_issue_uid = collections.defaultdict(list)
727 changes_by_ref_issue_uid = collections.defaultdict(list)
728 changes_without_issue = []
729 for change in self.changes:
730 added = False
731 for issue_uid in change['bugs']:
732 if issue_uid in issues:
733 changes_by_issue_uid[issue_uid].append(change)
734 added = True
735 if issue_uid in ref_issues:
736 changes_by_ref_issue_uid[issue_uid].append(change)
737 added = True
738 if not added:
739 changes_without_issue.append(change)
740
741 # Changes referencing own issues.
742 for issue_uid in issues:
743 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
744 self.print_issue(issues[issue_uid])
745 for change in changes_by_issue_uid[issue_uid]:
746 print '', # this prints one space due to comma, but no newline
747 self.print_change(change)
748
749 # Changes referencing others' issues.
750 for issue_uid in ref_issues:
751 assert changes_by_ref_issue_uid[issue_uid]
752 self.print_issue(ref_issues[issue_uid])
753 for change in changes_by_ref_issue_uid[issue_uid]:
754 print '', # this prints one space due to comma, but no newline
755 self.print_change(change)
756
757 # Changes referencing no issues.
758 if changes_without_issue:
759 print self.options.output_format_no_url.format(title='Other changes')
760 for change in changes_without_issue:
761 print '', # this prints one space due to comma, but no newline
762 self.print_change(change)
763
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000764 def print_activity(self):
765 self.print_changes()
766 self.print_reviews()
767 self.print_issues()
768
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000769 def dump_json(self, ignore_keys=None):
770 if ignore_keys is None:
771 ignore_keys = ['replies']
772
773 def format_for_json_dump(in_array):
774 output = {}
775 for item in in_array:
776 url = item.get('url') or item.get('review_url')
777 if not url:
778 raise Exception('Dumped item %s does not specify url' % item)
779 output[url] = dict(
780 (k, v) for k,v in item.iteritems() if k not in ignore_keys)
781 return output
782
783 class PythonObjectEncoder(json.JSONEncoder):
784 def default(self, obj): # pylint: disable=method-hidden
785 if isinstance(obj, datetime):
786 return obj.isoformat()
787 if isinstance(obj, set):
788 return list(obj)
789 return json.JSONEncoder.default(self, obj)
790
791 output = {
792 'reviews': format_for_json_dump(self.reviews),
793 'changes': format_for_json_dump(self.changes),
794 'issues': format_for_json_dump(self.issues)
795 }
796 print json.dumps(output, indent=2, cls=PythonObjectEncoder)
797
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000798
799def main():
800 # Silence upload.py.
801 rietveld.upload.verbosity = 0
802
803 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
804 parser.add_option(
805 '-u', '--user', metavar='<email>',
806 default=os.environ.get('USER'),
807 help='Filter on user, default=%default')
808 parser.add_option(
809 '-b', '--begin', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000810 help='Filter issues created after the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000811 parser.add_option(
812 '-e', '--end', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000813 help='Filter issues created before the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000814 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
815 relativedelta(months=2))
816 parser.add_option(
817 '-Q', '--last_quarter', action='store_true',
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000818 help='Use last quarter\'s dates, i.e. %s to %s' % (
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000819 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
820 parser.add_option(
821 '-Y', '--this_year', action='store_true',
822 help='Use this year\'s dates')
823 parser.add_option(
824 '-w', '--week_of', metavar='<date>',
wychen@chromium.org85cab632015-05-28 21:04:37 +0000825 help='Show issues for week of the date (mm/dd/yy)')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000826 parser.add_option(
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000827 '-W', '--last_week', action='count',
828 help='Show last week\'s issues. Use more times for more weeks.')
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000829 parser.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000830 '-a', '--auth',
831 action='store_true',
832 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000833 parser.add_option(
834 '-d', '--deltas',
835 action='store_true',
Nicolas Boichat23c165f2018-01-26 10:04:27 +0800836 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100837 parser.add_option(
838 '--no-referenced-issues',
839 action='store_true',
840 help='Do not fetch issues referenced by owned changes. Useful in '
841 'combination with --changes-by-issue when you only want to list '
842 'issues that are your own in the output.')
843 parser.add_option(
844 '--skip-own-issues-without-changes',
845 action='store_true',
846 help='Skips listing own issues without changes when showing changes '
847 'grouped by referenced issue(s). See --changes-by-issue for more '
848 'details.')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000849
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000850 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000851 'By default, all activity will be looked up and '
852 'printed. If any of these are specified, only '
853 'those specified will be searched.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000854 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000855 '-c', '--changes',
856 action='store_true',
857 help='Show changes.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000858 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000859 '-i', '--issues',
860 action='store_true',
861 help='Show issues.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000862 activity_types_group.add_option(
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000863 '-r', '--reviews',
864 action='store_true',
865 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100866 activity_types_group.add_option(
867 '--changes-by-issue', action='store_true',
868 help='Show changes grouped by referenced issue(s).')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000869 parser.add_option_group(activity_types_group)
870
871 output_format_group = optparse.OptionGroup(parser, 'Output Format',
872 'By default, all activity will be printed in the '
873 'following format: {url} {title}. This can be '
874 'changed for either all activity types or '
875 'individually for each activity type. The format '
876 'is defined as documented for '
877 'string.format(...). The variables available for '
878 'all activity types are url, title and author. '
879 'Format options for specific activity types will '
880 'override the generic format.')
881 output_format_group.add_option(
882 '-f', '--output-format', metavar='<format>',
883 default=u'{url} {title}',
884 help='Specifies the format to use when printing all your activity.')
885 output_format_group.add_option(
886 '--output-format-changes', metavar='<format>',
887 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000888 help='Specifies the format to use when printing changes. Supports the '
889 'additional variable {reviewers}')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000890 output_format_group.add_option(
891 '--output-format-issues', metavar='<format>',
892 default=None,
cjhopman@chromium.org53c1e562013-03-11 20:02:38 +0000893 help='Specifies the format to use when printing issues. Supports the '
894 'additional variable {owner}.')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000895 output_format_group.add_option(
896 '--output-format-reviews', metavar='<format>',
897 default=None,
898 help='Specifies the format to use when printing reviews.')
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000899 output_format_group.add_option(
900 '--output-format-heading', metavar='<format>',
901 default=u'{heading}:',
902 help='Specifies the format to use when printing headings.')
903 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100904 '--output-format-no-url', default='{title}',
905 help='Specifies the format to use when printing activity without url.')
906 output_format_group.add_option(
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000907 '-m', '--markdown', action='store_true',
908 help='Use markdown-friendly output (overrides --output-format '
909 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000910 output_format_group.add_option(
911 '-j', '--json', action='store_true',
912 help='Output json data (overrides other format options)')
nyquist@chromium.org18bc90d2012-12-20 19:26:47 +0000913 parser.add_option_group(output_format_group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000914 auth.add_auth_options(parser)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000915
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000916 parser.add_option(
917 '-v', '--verbose',
918 action='store_const',
919 dest='verbosity',
920 default=logging.WARN,
921 const=logging.INFO,
922 help='Output extra informational messages.'
923 )
924 parser.add_option(
925 '-q', '--quiet',
926 action='store_const',
927 dest='verbosity',
928 const=logging.ERROR,
929 help='Suppress non-error messages.'
930 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +0000931 parser.add_option(
932 '-o', '--output', metavar='<file>',
933 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000934
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000935 # Remove description formatting
936 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800937 lambda _: parser.description) # pylint: disable=no-member
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000938
939 options, args = parser.parse_args()
940 options.local_user = os.environ.get('USER')
941 if args:
942 parser.error('Args unsupported')
943 if not options.user:
944 parser.error('USER is not set, please use -u')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000945 options.user = username(options.user)
946
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000947 logging.basicConfig(level=options.verbosity)
948
949 # python-keyring provides easy access to the system keyring.
950 try:
951 import keyring # pylint: disable=unused-import,unused-variable,F0401
952 except ImportError:
953 logging.warning('Consider installing python-keyring')
954
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000955 if not options.begin:
956 if options.last_quarter:
957 begin, end = quarter_begin, quarter_end
958 elif options.this_year:
959 begin, end = get_year_of(datetime.today())
960 elif options.week_of:
961 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
jsbell@chromium.org74bfde02014-04-09 17:14:54 +0000962 elif options.last_week:
wychen@chromium.org8ba1ddb2015-04-29 00:04:25 +0000963 begin, end = (get_week_of(datetime.today() -
964 timedelta(days=1 + 7 * options.last_week)))
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000965 else:
966 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
967 else:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700968 begin = dateutil.parser.parse(options.begin)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000969 if options.end:
Daniel Cheng4b37ce62017-09-07 12:00:02 -0700970 end = dateutil.parser.parse(options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000971 else:
972 end = datetime.today()
973 options.begin, options.end = begin, end
974
jsbell@chromium.orgc92f5822014-01-06 23:49:11 +0000975 if options.markdown:
976 options.output_format = ' * [{title}]({url})'
977 options.output_format_heading = '### {heading} ###'
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100978 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000979 logging.info('Searching for activity by %s', options.user)
980 logging.info('Using range %s to %s', options.begin, options.end)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000981
982 my_activity = MyActivity(options)
983
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100984 if not (options.changes or options.reviews or options.issues or
985 options.changes_by_issue):
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000986 options.changes = True
987 options.issues = True
988 options.reviews = True
989
990 # First do any required authentication so none of the user interaction has to
991 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +0100992 if options.changes or options.changes_by_issue:
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000993 my_activity.auth_for_changes()
994 if options.reviews:
995 my_activity.auth_for_reviews()
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000996
Tobias Sargeantffb3c432017-03-08 14:09:14 +0000997 logging.info('Looking up activity.....')
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +0000998
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +0000999 try:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001000 if options.changes or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001001 my_activity.get_changes()
1002 if options.reviews:
1003 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001004 if options.issues or options.changes_by_issue:
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001005 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001006 if not options.no_referenced_issues:
1007 my_activity.get_referenced_issues()
seanmccullough@chromium.org3e4a5812015-06-11 17:48:47 +00001008 except auth.AuthenticationError as e:
Tobias Sargeantffb3c432017-03-08 14:09:14 +00001009 logging.error('auth.AuthenticationError: %s', e)
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001010
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +00001011 output_file = None
1012 try:
1013 if options.output:
1014 output_file = open(options.output, 'w')
1015 logging.info('Printing output to "%s"', options.output)
1016 sys.stdout = output_file
1017 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 12:55:01 -07001018 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +00001019 else:
1020 if options.json:
1021 my_activity.dump_json()
1022 else:
Sergiy Byelozyorov544b7442018-03-16 21:44:58 +01001023 if options.changes:
1024 my_activity.print_changes()
1025 if options.reviews:
1026 my_activity.print_reviews()
1027 if options.issues:
1028 my_activity.print_issues()
1029 if options.changes_by_issue:
1030 my_activity.print_changes_by_issue(
1031 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47 +00001032 finally:
1033 if output_file:
1034 logging.info('Done printing to file.')
1035 sys.stdout = sys.__stdout__
1036 output_file.close()
1037
cjhopman@chromium.org04d119d2012-10-17 22:41:53 +00001038 return 0
1039
1040
1041if __name__ == '__main__':
stevefung@chromium.org832d51e2015-05-27 12:52:51 +00001042 # Fix encoding to support non-ascii issue titles.
1043 fix_encoding.fix_encoding()
1044
sbc@chromium.org013731e2015-02-26 18:28:43 +00001045 try:
1046 sys.exit(main())
1047 except KeyboardInterrupt:
1048 sys.stderr.write('interrupted\n')
1049 sys.exit(1)