blob: 1c775ffc7db94576820e70f354ea306e2f6dbaab [file] [log] [blame]
[email protected]04d119d2012-10-17 22:41:531#!/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:4714 - my_activity.py -jd to output stats for the week to json with deltas data.
[email protected]04d119d2012-10-17 22:41:5315"""
16
[email protected]04d119d2012-10-17 22:41:5317# 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 20:44:5824import collections
Sergiy Byelozyorov1b7d56d2018-03-21 16:07:2825import contextlib
[email protected]04d119d2012-10-17 22:41:5326from datetime import datetime
27from datetime import timedelta
28from functools import partial
Sergiy Byelozyorov544b7442018-03-16 20:44:5829import itertools
[email protected]04d119d2012-10-17 22:41:5330import json
Tobias Sargeantffb3c432017-03-08 14:09:1431import logging
Sergiy Byelozyorov1b7d56d2018-03-21 16:07:2832from multiprocessing.pool import ThreadPool
[email protected]04d119d2012-10-17 22:41:5333import optparse
34import os
35import subprocess
Tobias Sargeantffb3c432017-03-08 14:09:1436from string import Formatter
[email protected]04d119d2012-10-17 22:41:5337import sys
38import urllib
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:4739import re
[email protected]04d119d2012-10-17 22:41:5340
[email protected]cf6a5d22015-04-09 22:02:0041import auth
[email protected]832d51e2015-05-27 12:52:5142import fix_encoding
[email protected]f8be2762013-11-06 01:01:5943import gerrit_util
[email protected]04d119d2012-10-17 22:41:5344import rietveld
[email protected]04d119d2012-10-17 22:41:5345
[email protected]3e4a5812015-06-11 17:48:4746from third_party import httplib2
47
[email protected]04d119d2012-10-17 22:41:5348try:
Tobias Sargeantffb3c432017-03-08 14:09:1449 import dateutil # pylint: disable=import-error
50 import dateutil.parser
51 from dateutil.relativedelta import relativedelta
[email protected]04d119d2012-10-17 22:41:5352except ImportError:
Tobias Sargeantffb3c432017-03-08 14:09:1453 logging.error('python-dateutil package required')
[email protected]04d119d2012-10-17 22:41:5354 exit(1)
55
Tobias Sargeantffb3c432017-03-08 14:09:1456
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)
[email protected]04d119d2012-10-17 22:41:5366
[email protected]04d119d2012-10-17 22:41:5367rietveld_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 19:55:0174 'short_url_protocol': 'https',
[email protected]04d119d2012-10-17 22:41:5375 },
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 {
Adrienne Walker95d4c852018-09-27 20:28:1299 'url': 'android-review.googlesource.com',
[email protected]04d119d2012-10-17 22:41:53100 },
[email protected]f8be2762013-11-06 01:01:59101 {
102 'url': 'chrome-internal-review.googlesource.com',
Jeremy Romanf475a472017-06-06 20:49:11103 'shorturl': 'crrev.com/i',
Varun Khanejad9f97bc2017-08-02 19:55:01104 'short_url_protocol': 'https',
[email protected]f8be2762013-11-06 01:01:59105 },
[email protected]56dc57a2015-09-10 18:26:54106 {
Adrienne Walker95d4c852018-09-27 20:28:12107 'url': 'chromium-review.googlesource.com',
108 'shorturl': 'crrev.com/c',
109 'short_url_protocol': 'https',
[email protected]56dc57a2015-09-10 18:26:54110 },
Ryan Harrison897602a2017-09-18 20:23:41111 {
112 'url': 'pdfium-review.googlesource.com',
113 },
Adrienne Walker95d4c852018-09-27 20:28:12114 {
115 'url': 'skia-review.googlesource.com',
116 },
[email protected]04d119d2012-10-17 22:41:53117]
118
Sergiy Byelozyorov544b7442018-03-16 20:44:58119monorail_projects = {
120 'chromium': {
[email protected]04d119d2012-10-17 22:41:53121 'shorturl': 'crbug.com',
Varun Khanejad9f97bc2017-08-02 19:55:01122 'short_url_protocol': 'https',
[email protected]04d119d2012-10-17 22:41:53123 },
Sergiy Byelozyorov544b7442018-03-16 20:44:58124 'google-breakpad': {},
125 'gyp': {},
126 'skia': {},
127 'pdfium': {
Ryan Harrison897602a2017-09-18 20:23:41128 'shorturl': 'crbug.com/pdfium',
129 'short_url_protocol': 'https',
130 },
Sergiy Byelozyorov544b7442018-03-16 20:44:58131 'v8': {
132 'shorturl': 'crbug.com/v8',
133 'short_url_protocol': 'https',
134 },
135}
[email protected]04d119d2012-10-17 22:41:53136
[email protected]04d119d2012-10-17 22:41:53137def username(email):
138 """Keeps the username of an email address."""
139 return email and email.split('@', 1)[0]
140
141
[email protected]426557a2012-10-22 20:18:52142def datetime_to_midnight(date):
143 return date - timedelta(hours=date.hour, minutes=date.minute,
144 seconds=date.second, microseconds=date.microsecond)
145
146
[email protected]04d119d2012-10-17 22:41:53147def get_quarter_of(date):
[email protected]426557a2012-10-22 20:18:52148 begin = (datetime_to_midnight(date) -
149 relativedelta(months=(date.month % 3) - 1, days=(date.day - 1)))
[email protected]04d119d2012-10-17 22:41:53150 return begin, begin + relativedelta(months=3)
151
152
153def get_year_of(date):
[email protected]426557a2012-10-22 20:18:52154 begin = (datetime_to_midnight(date) -
155 relativedelta(months=(date.month - 1), days=(date.day - 1)))
[email protected]04d119d2012-10-17 22:41:53156 return begin, begin + relativedelta(years=1)
157
158
159def get_week_of(date):
[email protected]426557a2012-10-22 20:18:52160 begin = (datetime_to_midnight(date) - timedelta(days=date.weekday()))
[email protected]04d119d2012-10-17 22:41:53161 return begin, begin + timedelta(days=7)
162
163
164def get_yes_or_no(msg):
165 while True:
166 response = raw_input(msg + ' yes/no [no] ')
167 if response == 'y' or response == 'yes':
168 return True
169 elif not response or response == 'n' or response == 'no':
170 return False
171
172
[email protected]6c039202013-09-12 12:28:12173def datetime_from_gerrit(date_string):
174 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f000')
175
176
[email protected]04d119d2012-10-17 22:41:53177def datetime_from_rietveld(date_string):
[email protected]29eb6e62014-03-20 01:55:55178 try:
179 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f')
180 except ValueError:
181 # Sometimes rietveld returns a value without the milliseconds part, so we
182 # attempt to parse those cases as well.
183 return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S')
[email protected]04d119d2012-10-17 22:41:53184
185
Sergiy Byelozyorov1b7d56d2018-03-21 16:07:28186def datetime_from_monorail(date_string):
187 return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S')
[email protected]04d119d2012-10-17 22:41:53188
189
190class MyActivity(object):
191 def __init__(self, options):
192 self.options = options
193 self.modified_after = options.begin
194 self.modified_before = options.end
195 self.user = options.user
196 self.changes = []
197 self.reviews = []
198 self.issues = []
Sergiy Byelozyorov544b7442018-03-16 20:44:58199 self.referenced_issues = []
[email protected]04d119d2012-10-17 22:41:53200 self.check_cookies()
201 self.google_code_auth_token = None
Vadim Bendebury8de38002018-05-15 02:02:55202 self.access_errors = set()
[email protected]04d119d2012-10-17 22:41:53203
Sergiy Byelozyorova68d82c2018-03-21 16:20:56204 def show_progress(self, how='.'):
205 if sys.stdout.isatty():
206 sys.stdout.write(how)
207 sys.stdout.flush()
208
[email protected]04d119d2012-10-17 22:41:53209 # Check the codereview cookie jar to determine which Rietveld instances to
210 # authenticate to.
211 def check_cookies(self):
[email protected]04d119d2012-10-17 22:41:53212 filtered_instances = []
213
214 def has_cookie(instance):
[email protected]3e4a5812015-06-11 17:48:47215 auth_config = auth.extract_auth_config_from_options(self.options)
216 a = auth.get_authenticator_for_host(instance['url'], auth_config)
217 return a.has_cached_credentials()
[email protected]04d119d2012-10-17 22:41:53218
219 for instance in rietveld_instances:
220 instance['auth'] = has_cookie(instance)
221
222 if filtered_instances:
Tobias Sargeantffb3c432017-03-08 14:09:14223 logging.warning('No cookie found for the following Rietveld instance%s:',
224 's' if len(filtered_instances) > 1 else '')
[email protected]04d119d2012-10-17 22:41:53225 for instance in filtered_instances:
Tobias Sargeantffb3c432017-03-08 14:09:14226 logging.warning('\t' + instance['url'])
227 logging.warning('Use --auth if you would like to authenticate to them.')
[email protected]04d119d2012-10-17 22:41:53228
229 def rietveld_search(self, instance, owner=None, reviewer=None):
230 if instance['requires_auth'] and not instance['auth']:
231 return []
232
233
234 email = None if instance['auth'] else ''
[email protected]cf6a5d22015-04-09 22:02:00235 auth_config = auth.extract_auth_config_from_options(self.options)
236 remote = rietveld.Rietveld('https://' + instance['url'], auth_config, email)
[email protected]04d119d2012-10-17 22:41:53237
238 # See def search() in rietveld.py to see all the filters you can use.
239 query_modified_after = None
240
241 if instance['supports_owner_modified_query']:
242 query_modified_after = self.modified_after.strftime('%Y-%m-%d')
243
244 # Rietveld does not allow search by both created_before and modified_after.
245 # (And some instances don't allow search by both owner and modified_after)
246 owner_email = None
247 reviewer_email = None
248 if owner:
249 owner_email = owner + '@' + instance['email_domain']
250 if reviewer:
251 reviewer_email = reviewer + '@' + instance['email_domain']
252 issues = remote.search(
253 owner=owner_email,
254 reviewer=reviewer_email,
255 modified_after=query_modified_after,
256 with_messages=True)
Sergiy Byelozyorova68d82c2018-03-21 16:20:56257 self.show_progress()
[email protected]04d119d2012-10-17 22:41:53258
259 issues = filter(
260 lambda i: (datetime_from_rietveld(i['created']) < self.modified_before),
261 issues)
262 issues = filter(
263 lambda i: (datetime_from_rietveld(i['modified']) > self.modified_after),
264 issues)
265
266 should_filter_by_user = True
Tobias Sargeantffb3c432017-03-08 14:09:14267 issues = map(partial(self.process_rietveld_issue, remote, instance), issues)
[email protected]04d119d2012-10-17 22:41:53268 issues = filter(
269 partial(self.filter_issue, should_filter_by_user=should_filter_by_user),
270 issues)
271 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
272
273 return issues
274
Andrii Shyshkalov024a3312018-06-29 21:59:06275 def extract_bug_numbers_from_description(self, issue):
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47276 description = None
277
278 if 'description' in issue:
279 # Getting the description for Rietveld
280 description = issue['description']
281 elif 'revisions' in issue:
282 # Getting the description for REST Gerrit
283 revision = issue['revisions'][issue['current_revision']]
284 description = revision['commit']['message']
285
286 bugs = []
287 if description:
Nicolas Dossou-gbete903ea732017-07-10 15:46:59288 # Handle both "Bug: 99999" and "BUG=99999" bug notations
289 # Multiple bugs can be noted on a single line or in multiple ones.
Sergiy Byelozyorov544b7442018-03-16 20:44:58290 matches = re.findall(
291 r'BUG[=:]\s?((((?:[a-zA-Z0-9-]+:)?\d+)(,\s?)?)+)', description,
292 flags=re.IGNORECASE)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47293 if matches:
294 for match in matches:
295 bugs.extend(match[0].replace(' ', '').split(','))
Sergiy Byelozyorov544b7442018-03-16 20:44:58296 # Add default chromium: prefix if none specified.
297 bugs = [bug if ':' in bug else 'chromium:%s' % bug for bug in bugs]
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47298
Andrii Shyshkalov024a3312018-06-29 21:59:06299 return sorted(set(bugs))
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47300
Tobias Sargeantffb3c432017-03-08 14:09:14301 def process_rietveld_issue(self, remote, instance, issue):
[email protected]04d119d2012-10-17 22:41:53302 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14303 if self.options.deltas:
304 patchset_props = remote.get_patchset_properties(
305 issue['issue'],
306 issue['patchsets'][-1])
Sergiy Byelozyorova68d82c2018-03-21 16:20:56307 self.show_progress()
Tobias Sargeantffb3c432017-03-08 14:09:14308 ret['delta'] = '+%d,-%d' % (
309 sum(f['num_added'] for f in patchset_props['files'].itervalues()),
310 sum(f['num_removed'] for f in patchset_props['files'].itervalues()))
311
312 if issue['landed_days_ago'] != 'unknown':
313 ret['status'] = 'committed'
314 elif issue['closed']:
315 ret['status'] = 'closed'
316 elif len(issue['reviewers']) and issue['all_required_reviewers_approved']:
317 ret['status'] = 'ready'
318 else:
319 ret['status'] = 'open'
320
[email protected]04d119d2012-10-17 22:41:53321 ret['owner'] = issue['owner_email']
322 ret['author'] = ret['owner']
323
[email protected]53c1e562013-03-11 20:02:38324 ret['reviewers'] = set(issue['reviewers'])
[email protected]04d119d2012-10-17 22:41:53325
[email protected]04d119d2012-10-17 22:41:53326 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 19:55:01327 url = instance['shorturl']
328 protocol = instance.get('short_url_protocol', 'http')
329 else:
330 url = instance['url']
331 protocol = 'https'
[email protected]04d119d2012-10-17 22:41:53332
Varun Khanejad9f97bc2017-08-02 19:55:01333 ret['review_url'] = '%s://%s/%d' % (protocol, url, issue['issue'])
[email protected]53c1e562013-03-11 20:02:38334
335 # Rietveld sometimes has '\r\n' instead of '\n'.
336 ret['header'] = issue['description'].replace('\r', '').split('\n')[0]
[email protected]04d119d2012-10-17 22:41:53337
338 ret['modified'] = datetime_from_rietveld(issue['modified'])
339 ret['created'] = datetime_from_rietveld(issue['created'])
340 ret['replies'] = self.process_rietveld_replies(issue['messages'])
341
Andrii Shyshkalov024a3312018-06-29 21:59:06342 ret['bugs'] = self.extract_bug_numbers_from_description(issue)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47343 ret['landed_days_ago'] = issue['landed_days_ago']
344
[email protected]04d119d2012-10-17 22:41:53345 return ret
346
347 @staticmethod
348 def process_rietveld_replies(replies):
349 ret = []
350 for reply in replies:
351 r = {}
352 r['author'] = reply['sender']
353 r['created'] = datetime_from_rietveld(reply['date'])
354 r['content'] = ''
355 ret.append(r)
356 return ret
357
Vadim Bendebury8de38002018-05-15 02:02:55358 def gerrit_changes_over_rest(self, instance, filters):
Michael Achenbach6fbf12f2017-07-06 08:54:11359 # Convert the "key:value" filter to a list of (key, value) pairs.
360 req = list(f.split(':', 1) for f in filters)
[email protected]6c039202013-09-12 12:28:12361 try:
[email protected]f8be2762013-11-06 01:01:59362 # Instantiate the generator to force all the requests now and catch the
363 # errors here.
364 return list(gerrit_util.GenerateAllChanges(instance['url'], req,
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47365 o_params=['MESSAGES', 'LABELS', 'DETAILED_ACCOUNTS',
366 'CURRENT_REVISION', 'CURRENT_COMMIT']))
[email protected]f8be2762013-11-06 01:01:59367 except gerrit_util.GerritError, e:
Vadim Bendebury8de38002018-05-15 02:02:55368 error_message = 'Looking up %r: %s' % (instance['url'], e)
369 if error_message not in self.access_errors:
370 self.access_errors.add(error_message)
[email protected]6c039202013-09-12 12:28:12371 return []
372
[email protected]6c039202013-09-12 12:28:12373 def gerrit_search(self, instance, owner=None, reviewer=None):
374 max_age = datetime.today() - self.modified_after
Andrii Shyshkalov4aedec02018-09-17 16:31:17375 filters = ['-age:%ss' % (max_age.days * 24 * 3600 + max_age.seconds)]
376 if owner:
377 assert not reviewer
378 filters.append('owner:%s' % owner)
379 else:
380 filters.extend(('-owner:%s' % reviewer, 'reviewer:%s' % reviewer))
Peter K. Lee9342ac02018-08-28 21:03:51381 # TODO(cjhopman): Should abandoned changes be filtered out when
382 # merged_only is not enabled?
383 if self.options.merged_only:
384 filters.append('status:merged')
[email protected]6c039202013-09-12 12:28:12385
Aaron Gable2979a872017-09-06 00:38:32386 issues = self.gerrit_changes_over_rest(instance, filters)
Sergiy Byelozyorova68d82c2018-03-21 16:20:56387 self.show_progress()
Aaron Gable2979a872017-09-06 00:38:32388 issues = [self.process_gerrit_issue(instance, issue)
389 for issue in issues]
[email protected]04d119d2012-10-17 22:41:53390
[email protected]04d119d2012-10-17 22:41:53391 issues = filter(self.filter_issue, issues)
392 issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
393
394 return issues
395
Aaron Gable2979a872017-09-06 00:38:32396 def process_gerrit_issue(self, instance, issue):
[email protected]6c039202013-09-12 12:28:12397 ret = {}
Tobias Sargeantffb3c432017-03-08 14:09:14398 if self.options.deltas:
399 ret['delta'] = DefaultFormatter().format(
400 '+{insertions},-{deletions}',
401 **issue)
402 ret['status'] = issue['status']
[email protected]6c039202013-09-12 12:28:12403 if 'shorturl' in instance:
Varun Khanejad9f97bc2017-08-02 19:55:01404 protocol = instance.get('short_url_protocol', 'http')
405 url = instance['shorturl']
406 else:
407 protocol = 'https'
408 url = instance['url']
409 ret['review_url'] = '%s://%s/%s' % (protocol, url, issue['_number'])
410
[email protected]6c039202013-09-12 12:28:12411 ret['header'] = issue['subject']
Don Garrett2ebf9fd2018-08-06 15:14:00412 ret['owner'] = issue['owner'].get('email', '')
[email protected]6c039202013-09-12 12:28:12413 ret['author'] = ret['owner']
414 ret['created'] = datetime_from_gerrit(issue['created'])
415 ret['modified'] = datetime_from_gerrit(issue['updated'])
416 if 'messages' in issue:
Aaron Gable2979a872017-09-06 00:38:32417 ret['replies'] = self.process_gerrit_issue_replies(issue['messages'])
[email protected]6c039202013-09-12 12:28:12418 else:
419 ret['replies'] = []
420 ret['reviewers'] = set(r['author'] for r in ret['replies'])
421 ret['reviewers'].discard(ret['author'])
Andrii Shyshkalov024a3312018-06-29 21:59:06422 ret['bugs'] = self.extract_bug_numbers_from_description(issue)
[email protected]6c039202013-09-12 12:28:12423 return ret
424
425 @staticmethod
Aaron Gable2979a872017-09-06 00:38:32426 def process_gerrit_issue_replies(replies):
[email protected]6c039202013-09-12 12:28:12427 ret = []
[email protected]f8be2762013-11-06 01:01:59428 replies = filter(lambda r: 'author' in r and 'email' in r['author'],
429 replies)
[email protected]6c039202013-09-12 12:28:12430 for reply in replies:
431 ret.append({
432 'author': reply['author']['email'],
433 'created': datetime_from_gerrit(reply['date']),
434 'content': reply['message'],
435 })
[email protected]04d119d2012-10-17 22:41:53436 return ret
437
Sergiy Byelozyorov1b7d56d2018-03-21 16:07:28438 def monorail_get_auth_http(self):
[email protected]3e4a5812015-06-11 17:48:47439 auth_config = auth.extract_auth_config_from_options(self.options)
440 authenticator = auth.get_authenticator_for_host(
Tobias Sargeantffb3c432017-03-08 14:09:14441 'bugs.chromium.org', auth_config)
Kenneth Russell66badbd2018-09-09 21:35:32442 # Manually use a long timeout (10m); for some users who have a
443 # long history on the issue tracker, whatever the default timeout
444 # is is reached.
445 return authenticator.authorize(httplib2.Http(timeout=600))
Sergiy Byelozyorov1b7d56d2018-03-21 16:07:28446
447 def filter_modified_monorail_issue(self, issue):
448 """Precisely checks if an issue has been modified in the time range.
449
450 This fetches all issue comments to check if the issue has been modified in
451 the time range specified by user. This is needed because monorail only
452 allows filtering by last updated and published dates, which is not
453 sufficient to tell whether a given issue has been modified at some specific
454 time range. Any update to the issue is a reported as comment on Monorail.
455
456 Args:
457 issue: Issue dict as returned by monorail_query_issues method. In
458 particular, must have a key 'uid' formatted as 'project:issue_id'.
459
460 Returns:
461 Passed issue if modified, None otherwise.
462 """
463 http = self.monorail_get_auth_http()
464 project, issue_id = issue['uid'].split(':')
465 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
466 '/%s/issues/%s/comments?maxResults=10000') % (project, issue_id)
467 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 16:20:56468 self.show_progress()
Sergiy Byelozyorov1b7d56d2018-03-21 16:07:28469 content = json.loads(body)
470 if not content:
471 logging.error('Unable to parse %s response from monorail.', project)
472 return issue
473
474 for item in content.get('items', []):
475 comment_published = datetime_from_monorail(item['published'])
476 if self.filter_modified(comment_published):
477 return issue
478
479 return None
480
481 def monorail_query_issues(self, project, query):
482 http = self.monorail_get_auth_http()
Tobias Sargeantffb3c432017-03-08 14:09:14483 url = ('https://monorail-prod.appspot.com/_ah/api/monorail/v1/projects'
Sergiy Byelozyorov544b7442018-03-16 20:44:58484 '/%s/issues') % project
485 query_data = urllib.urlencode(query)
486 url = url + '?' + query_data
487 _, body = http.request(url)
Sergiy Byelozyorova68d82c2018-03-21 16:20:56488 self.show_progress()
Sergiy Byelozyorov544b7442018-03-16 20:44:58489 content = json.loads(body)
490 if not content:
Sergiy Byelozyorov1b7d56d2018-03-21 16:07:28491 logging.error('Unable to parse %s response from monorail.', project)
Sergiy Byelozyorov544b7442018-03-16 20:44:58492 return []
493
494 issues = []
Sergiy Byelozyorov1b7d56d2018-03-21 16:07:28495 project_config = monorail_projects.get(project, {})
Sergiy Byelozyorov544b7442018-03-16 20:44:58496 for item in content.get('items', []):
497 if project_config.get('shorturl'):
498 protocol = project_config.get('short_url_protocol', 'http')
499 item_url = '%s://%s/%d' % (
500 protocol, project_config['shorturl'], item['id'])
501 else:
502 item_url = 'https://bugs.chromium.org/p/%s/issues/detail?id=%d' % (
503 project, item['id'])
504 issue = {
505 'uid': '%s:%s' % (project, item['id']),
506 'header': item['title'],
Sergiy Byelozyorov1b7d56d2018-03-21 16:07:28507 'created': datetime_from_monorail(item['published']),
508 'modified': datetime_from_monorail(item['updated']),
Sergiy Byelozyorov544b7442018-03-16 20:44:58509 'author': item['author']['name'],
510 'url': item_url,
511 'comments': [],
512 'status': item['status'],
513 'labels': [],
514 'components': []
515 }
516 if 'owner' in item:
517 issue['owner'] = item['owner']['name']
518 else:
519 issue['owner'] = 'None'
520 if 'labels' in item:
521 issue['labels'] = item['labels']
522 if 'components' in item:
523 issue['components'] = item['components']
524 issues.append(issue)
525
526 return issues
527
528 def monorail_issue_search(self, project):
[email protected]3e4a5812015-06-11 17:48:47529 epoch = datetime.utcfromtimestamp(0)
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41530 # TODO(tandrii): support non-chromium email, too.
[email protected]3e4a5812015-06-11 17:48:47531 user_str = '%[email protected]' % self.user
[email protected]04d119d2012-10-17 22:41:53532
Sergiy Byelozyorov544b7442018-03-16 20:44:58533 issues = self.monorail_query_issues(project, {
[email protected]3e4a5812015-06-11 17:48:47534 'maxResults': 10000,
535 'q': user_str,
536 'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
537 'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
[email protected]04d119d2012-10-17 22:41:53538 })
[email protected]04d119d2012-10-17 22:41:53539
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41540 if self.options.completed_issues:
541 return [
542 issue for issue in issues
543 if (self.match(issue['owner']) and
544 issue['status'].lower() in ('verified', 'fixed'))
545 ]
546
Sergiy Byelozyorov544b7442018-03-16 20:44:58547 return [
548 issue for issue in issues
549 if issue['author'] == user_str or issue['owner'] == user_str]
[email protected]3e4a5812015-06-11 17:48:47550
Sergiy Byelozyorov544b7442018-03-16 20:44:58551 def monorail_get_issues(self, project, issue_ids):
552 return self.monorail_query_issues(project, {
553 'maxResults': 10000,
554 'q': 'id:%s' % ','.join(issue_ids)
555 })
[email protected]04d119d2012-10-17 22:41:53556
[email protected]c92f5822014-01-06 23:49:11557 def print_heading(self, heading):
558 print
559 print self.options.output_format_heading.format(heading=heading)
560
Tobias Sargeantffb3c432017-03-08 14:09:14561 def match(self, author):
562 if '@' in self.user:
563 return author == self.user
564 return author.startswith(self.user + '@')
565
[email protected]18bc90d2012-12-20 19:26:47566 def print_change(self, change):
Tobias Sargeantffb3c432017-03-08 14:09:14567 activity = len([
568 reply
569 for reply in change['replies']
570 if self.match(reply['author'])
571 ])
[email protected]53c1e562013-03-11 20:02:38572 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14573 'created': change['created'].date().isoformat(),
574 'modified': change['modified'].date().isoformat(),
575 'reviewers': ', '.join(change['reviewers']),
576 'status': change['status'],
577 'activity': activity,
[email protected]53c1e562013-03-11 20:02:38578 }
Tobias Sargeantffb3c432017-03-08 14:09:14579 if self.options.deltas:
580 optional_values['delta'] = change['delta']
581
[email protected]18bc90d2012-12-20 19:26:47582 self.print_generic(self.options.output_format,
583 self.options.output_format_changes,
584 change['header'],
585 change['review_url'],
[email protected]53c1e562013-03-11 20:02:38586 change['author'],
587 optional_values)
[email protected]18bc90d2012-12-20 19:26:47588
589 def print_issue(self, issue):
590 optional_values = {
Tobias Sargeantffb3c432017-03-08 14:09:14591 'created': issue['created'].date().isoformat(),
592 'modified': issue['modified'].date().isoformat(),
[email protected]18bc90d2012-12-20 19:26:47593 'owner': issue['owner'],
Tobias Sargeantffb3c432017-03-08 14:09:14594 'status': issue['status'],
[email protected]18bc90d2012-12-20 19:26:47595 }
596 self.print_generic(self.options.output_format,
597 self.options.output_format_issues,
598 issue['header'],
599 issue['url'],
600 issue['author'],
601 optional_values)
602
603 def print_review(self, review):
Tobias Sargeantffb3c432017-03-08 14:09:14604 activity = len([
605 reply
606 for reply in review['replies']
607 if self.match(reply['author'])
608 ])
609 optional_values = {
610 'created': review['created'].date().isoformat(),
611 'modified': review['modified'].date().isoformat(),
Nicolas Boichat23c165f2018-01-26 02:04:27612 'status': review['status'],
Tobias Sargeantffb3c432017-03-08 14:09:14613 'activity': activity,
614 }
Nicolas Boichat23c165f2018-01-26 02:04:27615 if self.options.deltas:
616 optional_values['delta'] = review['delta']
617
[email protected]18bc90d2012-12-20 19:26:47618 self.print_generic(self.options.output_format,
619 self.options.output_format_reviews,
620 review['header'],
621 review['review_url'],
Tobias Sargeantffb3c432017-03-08 14:09:14622 review['author'],
623 optional_values)
[email protected]04d119d2012-10-17 22:41:53624
625 @staticmethod
[email protected]18bc90d2012-12-20 19:26:47626 def print_generic(default_fmt, specific_fmt,
627 title, url, author,
628 optional_values=None):
629 output_format = specific_fmt if specific_fmt is not None else default_fmt
630 output_format = unicode(output_format)
Tobias Sargeantffb3c432017-03-08 14:09:14631 values = {
[email protected]18bc90d2012-12-20 19:26:47632 'title': title,
633 'url': url,
634 'author': author,
635 }
[email protected]18bc90d2012-12-20 19:26:47636 if optional_values is not None:
Tobias Sargeantffb3c432017-03-08 14:09:14637 values.update(optional_values)
638 print DefaultFormatter().format(output_format, **values).encode(
639 sys.getdefaultencoding())
[email protected]18bc90d2012-12-20 19:26:47640
[email protected]04d119d2012-10-17 22:41:53641
642 def filter_issue(self, issue, should_filter_by_user=True):
643 def maybe_filter_username(email):
644 return not should_filter_by_user or username(email) == self.user
645 if (maybe_filter_username(issue['author']) and
646 self.filter_modified(issue['created'])):
647 return True
648 if (maybe_filter_username(issue['owner']) and
649 (self.filter_modified(issue['created']) or
650 self.filter_modified(issue['modified']))):
651 return True
652 for reply in issue['replies']:
653 if self.filter_modified(reply['created']):
654 if not should_filter_by_user:
655 break
656 if (username(reply['author']) == self.user
657 or (self.user + '@') in reply['content']):
658 break
659 else:
660 return False
661 return True
662
663 def filter_modified(self, modified):
664 return self.modified_after < modified and modified < self.modified_before
665
666 def auth_for_changes(self):
667 #TODO(cjhopman): Move authentication check for getting changes here.
668 pass
669
670 def auth_for_reviews(self):
671 # Reviews use all the same instances as changes so no authentication is
672 # required.
673 pass
674
[email protected]04d119d2012-10-17 22:41:53675 def get_changes(self):
Sergiy Byelozyorov1b7d56d2018-03-21 16:07:28676 num_instances = len(rietveld_instances) + len(gerrit_instances)
677 with contextlib.closing(ThreadPool(num_instances)) as pool:
678 rietveld_changes = pool.map_async(
679 lambda instance: self.rietveld_search(instance, owner=self.user),
680 rietveld_instances)
681 gerrit_changes = pool.map_async(
682 lambda instance: self.gerrit_search(instance, owner=self.user),
683 gerrit_instances)
684 rietveld_changes = itertools.chain.from_iterable(rietveld_changes.get())
685 gerrit_changes = itertools.chain.from_iterable(gerrit_changes.get())
686 self.changes = list(rietveld_changes) + list(gerrit_changes)
[email protected]04d119d2012-10-17 22:41:53687
688 def print_changes(self):
689 if self.changes:
[email protected]c92f5822014-01-06 23:49:11690 self.print_heading('Changes')
[email protected]04d119d2012-10-17 22:41:53691 for change in self.changes:
Sergiy Byelozyorov544b7442018-03-16 20:44:58692 self.print_change(change)
[email protected]04d119d2012-10-17 22:41:53693
Vadim Bendebury8de38002018-05-15 02:02:55694 def print_access_errors(self):
695 if self.access_errors:
Ryan Harrison398fb442018-05-22 16:05:26696 logging.error('Access Errors:')
697 for error in self.access_errors:
698 logging.error(error.rstrip())
Vadim Bendebury8de38002018-05-15 02:02:55699
[email protected]04d119d2012-10-17 22:41:53700 def get_reviews(self):
Sergiy Byelozyorov1b7d56d2018-03-21 16:07:28701 num_instances = len(rietveld_instances) + len(gerrit_instances)
702 with contextlib.closing(ThreadPool(num_instances)) as pool:
703 rietveld_reviews = pool.map_async(
704 lambda instance: self.rietveld_search(instance, reviewer=self.user),
705 rietveld_instances)
706 gerrit_reviews = pool.map_async(
707 lambda instance: self.gerrit_search(instance, reviewer=self.user),
708 gerrit_instances)
709 rietveld_reviews = itertools.chain.from_iterable(rietveld_reviews.get())
710 gerrit_reviews = itertools.chain.from_iterable(gerrit_reviews.get())
Sergiy Byelozyorov1b7d56d2018-03-21 16:07:28711 self.reviews = list(rietveld_reviews) + list(gerrit_reviews)
[email protected]04d119d2012-10-17 22:41:53712
713 def print_reviews(self):
714 if self.reviews:
[email protected]c92f5822014-01-06 23:49:11715 self.print_heading('Reviews')
[email protected]04d119d2012-10-17 22:41:53716 for review in self.reviews:
[email protected]18bc90d2012-12-20 19:26:47717 self.print_review(review)
[email protected]04d119d2012-10-17 22:41:53718
719 def get_issues(self):
Sergiy Byelozyorov1b7d56d2018-03-21 16:07:28720 with contextlib.closing(ThreadPool(len(monorail_projects))) as pool:
721 monorail_issues = pool.map(
722 self.monorail_issue_search, monorail_projects.keys())
723 monorail_issues = list(itertools.chain.from_iterable(monorail_issues))
724
Vadim Bendeburycbf02042018-05-15 00:46:15725 if not monorail_issues:
726 return
727
Sergiy Byelozyorov1b7d56d2018-03-21 16:07:28728 with contextlib.closing(ThreadPool(len(monorail_issues))) as pool:
729 filtered_issues = pool.map(
730 self.filter_modified_monorail_issue, monorail_issues)
731 self.issues = [issue for issue in filtered_issues if issue]
Sergiy Byelozyorov544b7442018-03-16 20:44:58732
733 def get_referenced_issues(self):
734 if not self.issues:
735 self.get_issues()
736
737 if not self.changes:
738 self.get_changes()
739
740 referenced_issue_uids = set(itertools.chain.from_iterable(
741 change['bugs'] for change in self.changes))
742 fetched_issue_uids = set(issue['uid'] for issue in self.issues)
743 missing_issue_uids = referenced_issue_uids - fetched_issue_uids
744
745 missing_issues_by_project = collections.defaultdict(list)
746 for issue_uid in missing_issue_uids:
747 project, issue_id = issue_uid.split(':')
748 missing_issues_by_project[project].append(issue_id)
749
750 for project, issue_ids in missing_issues_by_project.iteritems():
751 self.referenced_issues += self.monorail_get_issues(project, issue_ids)
[email protected]04d119d2012-10-17 22:41:53752
[email protected]18bc90d2012-12-20 19:26:47753 def print_issues(self):
754 if self.issues:
[email protected]c92f5822014-01-06 23:49:11755 self.print_heading('Issues')
[email protected]18bc90d2012-12-20 19:26:47756 for issue in self.issues:
757 self.print_issue(issue)
758
Sergiy Byelozyorov544b7442018-03-16 20:44:58759 def print_changes_by_issue(self, skip_empty_own):
760 if not self.issues or not self.changes:
761 return
762
763 self.print_heading('Changes by referenced issue(s)')
764 issues = {issue['uid']: issue for issue in self.issues}
765 ref_issues = {issue['uid']: issue for issue in self.referenced_issues}
766 changes_by_issue_uid = collections.defaultdict(list)
767 changes_by_ref_issue_uid = collections.defaultdict(list)
768 changes_without_issue = []
769 for change in self.changes:
770 added = False
Andrii Shyshkalov483580b2018-07-02 06:55:10771 for issue_uid in change['bugs']:
Sergiy Byelozyorov544b7442018-03-16 20:44:58772 if issue_uid in issues:
773 changes_by_issue_uid[issue_uid].append(change)
774 added = True
775 if issue_uid in ref_issues:
776 changes_by_ref_issue_uid[issue_uid].append(change)
777 added = True
778 if not added:
779 changes_without_issue.append(change)
780
781 # Changes referencing own issues.
782 for issue_uid in issues:
783 if changes_by_issue_uid[issue_uid] or not skip_empty_own:
784 self.print_issue(issues[issue_uid])
Andrii Shyshkalovd4c2a872018-06-29 21:23:46785 if changes_by_issue_uid[issue_uid]:
786 print
787 for change in changes_by_issue_uid[issue_uid]:
788 print ' ', # this prints one space due to comma, but no newline
789 self.print_change(change)
790 print
Sergiy Byelozyorov544b7442018-03-16 20:44:58791
792 # Changes referencing others' issues.
793 for issue_uid in ref_issues:
794 assert changes_by_ref_issue_uid[issue_uid]
795 self.print_issue(ref_issues[issue_uid])
796 for change in changes_by_ref_issue_uid[issue_uid]:
797 print '', # this prints one space due to comma, but no newline
798 self.print_change(change)
799
800 # Changes referencing no issues.
801 if changes_without_issue:
802 print self.options.output_format_no_url.format(title='Other changes')
803 for change in changes_without_issue:
804 print '', # this prints one space due to comma, but no newline
805 self.print_change(change)
806
[email protected]04d119d2012-10-17 22:41:53807 def print_activity(self):
808 self.print_changes()
809 self.print_reviews()
810 self.print_issues()
811
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47812 def dump_json(self, ignore_keys=None):
813 if ignore_keys is None:
814 ignore_keys = ['replies']
815
816 def format_for_json_dump(in_array):
817 output = {}
818 for item in in_array:
819 url = item.get('url') or item.get('review_url')
820 if not url:
821 raise Exception('Dumped item %s does not specify url' % item)
822 output[url] = dict(
823 (k, v) for k,v in item.iteritems() if k not in ignore_keys)
824 return output
825
826 class PythonObjectEncoder(json.JSONEncoder):
827 def default(self, obj): # pylint: disable=method-hidden
828 if isinstance(obj, datetime):
829 return obj.isoformat()
830 if isinstance(obj, set):
831 return list(obj)
832 return json.JSONEncoder.default(self, obj)
833
834 output = {
835 'reviews': format_for_json_dump(self.reviews),
836 'changes': format_for_json_dump(self.changes),
837 'issues': format_for_json_dump(self.issues)
838 }
839 print json.dumps(output, indent=2, cls=PythonObjectEncoder)
840
[email protected]04d119d2012-10-17 22:41:53841
842def main():
843 # Silence upload.py.
844 rietveld.upload.verbosity = 0
845
846 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
847 parser.add_option(
848 '-u', '--user', metavar='<email>',
849 default=os.environ.get('USER'),
850 help='Filter on user, default=%default')
851 parser.add_option(
852 '-b', '--begin', metavar='<date>',
[email protected]85cab632015-05-28 21:04:37853 help='Filter issues created after the date (mm/dd/yy)')
[email protected]04d119d2012-10-17 22:41:53854 parser.add_option(
855 '-e', '--end', metavar='<date>',
[email protected]85cab632015-05-28 21:04:37856 help='Filter issues created before the date (mm/dd/yy)')
[email protected]04d119d2012-10-17 22:41:53857 quarter_begin, quarter_end = get_quarter_of(datetime.today() -
858 relativedelta(months=2))
859 parser.add_option(
860 '-Q', '--last_quarter', action='store_true',
[email protected]74bfde02014-04-09 17:14:54861 help='Use last quarter\'s dates, i.e. %s to %s' % (
[email protected]04d119d2012-10-17 22:41:53862 quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d')))
863 parser.add_option(
864 '-Y', '--this_year', action='store_true',
865 help='Use this year\'s dates')
866 parser.add_option(
867 '-w', '--week_of', metavar='<date>',
[email protected]85cab632015-05-28 21:04:37868 help='Show issues for week of the date (mm/dd/yy)')
[email protected]04d119d2012-10-17 22:41:53869 parser.add_option(
[email protected]8ba1ddb2015-04-29 00:04:25870 '-W', '--last_week', action='count',
871 help='Show last week\'s issues. Use more times for more weeks.')
[email protected]74bfde02014-04-09 17:14:54872 parser.add_option(
[email protected]04d119d2012-10-17 22:41:53873 '-a', '--auth',
874 action='store_true',
875 help='Ask to authenticate for instances with no auth cookie')
Tobias Sargeantffb3c432017-03-08 14:09:14876 parser.add_option(
877 '-d', '--deltas',
878 action='store_true',
Nicolas Boichat23c165f2018-01-26 02:04:27879 help='Fetch deltas for changes.')
Sergiy Byelozyorov544b7442018-03-16 20:44:58880 parser.add_option(
881 '--no-referenced-issues',
882 action='store_true',
883 help='Do not fetch issues referenced by owned changes. Useful in '
884 'combination with --changes-by-issue when you only want to list '
Sergiy Byelozyorovb4475ab2018-03-23 16:49:34885 'issues that have also been modified in the same time period.')
Sergiy Byelozyorov544b7442018-03-16 20:44:58886 parser.add_option(
887 '--skip-own-issues-without-changes',
888 action='store_true',
889 help='Skips listing own issues without changes when showing changes '
890 'grouped by referenced issue(s). See --changes-by-issue for more '
891 'details.')
[email protected]04d119d2012-10-17 22:41:53892
[email protected]18bc90d2012-12-20 19:26:47893 activity_types_group = optparse.OptionGroup(parser, 'Activity Types',
[email protected]04d119d2012-10-17 22:41:53894 'By default, all activity will be looked up and '
895 'printed. If any of these are specified, only '
896 'those specified will be searched.')
[email protected]18bc90d2012-12-20 19:26:47897 activity_types_group.add_option(
[email protected]04d119d2012-10-17 22:41:53898 '-c', '--changes',
899 action='store_true',
900 help='Show changes.')
[email protected]18bc90d2012-12-20 19:26:47901 activity_types_group.add_option(
[email protected]04d119d2012-10-17 22:41:53902 '-i', '--issues',
903 action='store_true',
904 help='Show issues.')
[email protected]18bc90d2012-12-20 19:26:47905 activity_types_group.add_option(
[email protected]04d119d2012-10-17 22:41:53906 '-r', '--reviews',
907 action='store_true',
908 help='Show reviews.')
Sergiy Byelozyorov544b7442018-03-16 20:44:58909 activity_types_group.add_option(
910 '--changes-by-issue', action='store_true',
911 help='Show changes grouped by referenced issue(s).')
[email protected]18bc90d2012-12-20 19:26:47912 parser.add_option_group(activity_types_group)
913
914 output_format_group = optparse.OptionGroup(parser, 'Output Format',
915 'By default, all activity will be printed in the '
916 'following format: {url} {title}. This can be '
917 'changed for either all activity types or '
918 'individually for each activity type. The format '
919 'is defined as documented for '
920 'string.format(...). The variables available for '
921 'all activity types are url, title and author. '
922 'Format options for specific activity types will '
923 'override the generic format.')
924 output_format_group.add_option(
925 '-f', '--output-format', metavar='<format>',
926 default=u'{url} {title}',
927 help='Specifies the format to use when printing all your activity.')
928 output_format_group.add_option(
929 '--output-format-changes', metavar='<format>',
930 default=None,
[email protected]53c1e562013-03-11 20:02:38931 help='Specifies the format to use when printing changes. Supports the '
932 'additional variable {reviewers}')
[email protected]18bc90d2012-12-20 19:26:47933 output_format_group.add_option(
934 '--output-format-issues', metavar='<format>',
935 default=None,
[email protected]53c1e562013-03-11 20:02:38936 help='Specifies the format to use when printing issues. Supports the '
937 'additional variable {owner}.')
[email protected]18bc90d2012-12-20 19:26:47938 output_format_group.add_option(
939 '--output-format-reviews', metavar='<format>',
940 default=None,
941 help='Specifies the format to use when printing reviews.')
[email protected]c92f5822014-01-06 23:49:11942 output_format_group.add_option(
943 '--output-format-heading', metavar='<format>',
944 default=u'{heading}:',
945 help='Specifies the format to use when printing headings.')
946 output_format_group.add_option(
Sergiy Byelozyorov544b7442018-03-16 20:44:58947 '--output-format-no-url', default='{title}',
948 help='Specifies the format to use when printing activity without url.')
949 output_format_group.add_option(
[email protected]c92f5822014-01-06 23:49:11950 '-m', '--markdown', action='store_true',
951 help='Use markdown-friendly output (overrides --output-format '
952 'and --output-format-heading)')
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47953 output_format_group.add_option(
954 '-j', '--json', action='store_true',
955 help='Output json data (overrides other format options)')
[email protected]18bc90d2012-12-20 19:26:47956 parser.add_option_group(output_format_group)
[email protected]cf6a5d22015-04-09 22:02:00957 auth.add_auth_options(parser)
[email protected]04d119d2012-10-17 22:41:53958
Tobias Sargeantffb3c432017-03-08 14:09:14959 parser.add_option(
960 '-v', '--verbose',
961 action='store_const',
962 dest='verbosity',
963 default=logging.WARN,
964 const=logging.INFO,
965 help='Output extra informational messages.'
966 )
967 parser.add_option(
968 '-q', '--quiet',
969 action='store_const',
970 dest='verbosity',
971 const=logging.ERROR,
972 help='Suppress non-error messages.'
973 )
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47974 parser.add_option(
Peter K. Lee9342ac02018-08-28 21:03:51975 '-M', '--merged-only',
976 action='store_true',
977 dest='merged_only',
978 default=False,
979 help='Shows only changes that have been merged.')
980 parser.add_option(
Andrii Shyshkalov8bdc1b82018-09-24 17:29:41981 '-C', '--completed-issues',
982 action='store_true',
983 dest='completed_issues',
984 default=False,
985 help='Shows only monorail issues that have completed (Fixed|Verified) '
986 'by the user.')
987 parser.add_option(
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:47988 '-o', '--output', metavar='<file>',
989 help='Where to output the results. By default prints to stdout.')
Tobias Sargeantffb3c432017-03-08 14:09:14990
[email protected]04d119d2012-10-17 22:41:53991 # Remove description formatting
992 parser.format_description = (
Quinten Yearsleyb2cc4a92016-12-15 21:53:26993 lambda _: parser.description) # pylint: disable=no-member
[email protected]04d119d2012-10-17 22:41:53994
995 options, args = parser.parse_args()
996 options.local_user = os.environ.get('USER')
997 if args:
998 parser.error('Args unsupported')
999 if not options.user:
1000 parser.error('USER is not set, please use -u')
[email protected]04d119d2012-10-17 22:41:531001 options.user = username(options.user)
1002
Tobias Sargeantffb3c432017-03-08 14:09:141003 logging.basicConfig(level=options.verbosity)
1004
1005 # python-keyring provides easy access to the system keyring.
1006 try:
1007 import keyring # pylint: disable=unused-import,unused-variable,F0401
1008 except ImportError:
1009 logging.warning('Consider installing python-keyring')
1010
[email protected]04d119d2012-10-17 22:41:531011 if not options.begin:
1012 if options.last_quarter:
1013 begin, end = quarter_begin, quarter_end
1014 elif options.this_year:
1015 begin, end = get_year_of(datetime.today())
1016 elif options.week_of:
1017 begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y')))
[email protected]74bfde02014-04-09 17:14:541018 elif options.last_week:
[email protected]8ba1ddb2015-04-29 00:04:251019 begin, end = (get_week_of(datetime.today() -
1020 timedelta(days=1 + 7 * options.last_week)))
[email protected]04d119d2012-10-17 22:41:531021 else:
1022 begin, end = (get_week_of(datetime.today() - timedelta(days=1)))
1023 else:
Daniel Cheng4b37ce62017-09-07 19:00:021024 begin = dateutil.parser.parse(options.begin)
[email protected]04d119d2012-10-17 22:41:531025 if options.end:
Daniel Cheng4b37ce62017-09-07 19:00:021026 end = dateutil.parser.parse(options.end)
[email protected]04d119d2012-10-17 22:41:531027 else:
1028 end = datetime.today()
1029 options.begin, options.end = begin, end
1030
[email protected]c92f5822014-01-06 23:49:111031 if options.markdown:
Andrii Shyshkalovd4c2a872018-06-29 21:23:461032 options.output_format_heading = '### {heading}\n'
1033 options.output_format = ' * [{title}]({url})'
1034 options.output_format_no_url = ' * {title}'
Tobias Sargeantffb3c432017-03-08 14:09:141035 logging.info('Searching for activity by %s', options.user)
1036 logging.info('Using range %s to %s', options.begin, options.end)
[email protected]04d119d2012-10-17 22:41:531037
1038 my_activity = MyActivity(options)
Sergiy Byelozyorova68d82c2018-03-21 16:20:561039 my_activity.show_progress('Loading data')
[email protected]04d119d2012-10-17 22:41:531040
Sergiy Byelozyorov544b7442018-03-16 20:44:581041 if not (options.changes or options.reviews or options.issues or
1042 options.changes_by_issue):
[email protected]04d119d2012-10-17 22:41:531043 options.changes = True
1044 options.issues = True
1045 options.reviews = True
1046
1047 # First do any required authentication so none of the user interaction has to
1048 # wait for actual work.
Sergiy Byelozyorov544b7442018-03-16 20:44:581049 if options.changes or options.changes_by_issue:
[email protected]04d119d2012-10-17 22:41:531050 my_activity.auth_for_changes()
1051 if options.reviews:
1052 my_activity.auth_for_reviews()
[email protected]04d119d2012-10-17 22:41:531053
Tobias Sargeantffb3c432017-03-08 14:09:141054 logging.info('Looking up activity.....')
[email protected]04d119d2012-10-17 22:41:531055
[email protected]3e4a5812015-06-11 17:48:471056 try:
Sergiy Byelozyorov544b7442018-03-16 20:44:581057 if options.changes or options.changes_by_issue:
[email protected]3e4a5812015-06-11 17:48:471058 my_activity.get_changes()
1059 if options.reviews:
1060 my_activity.get_reviews()
Sergiy Byelozyorov544b7442018-03-16 20:44:581061 if options.issues or options.changes_by_issue:
[email protected]3e4a5812015-06-11 17:48:471062 my_activity.get_issues()
Sergiy Byelozyorov544b7442018-03-16 20:44:581063 if not options.no_referenced_issues:
1064 my_activity.get_referenced_issues()
[email protected]3e4a5812015-06-11 17:48:471065 except auth.AuthenticationError as e:
Tobias Sargeantffb3c432017-03-08 14:09:141066 logging.error('auth.AuthenticationError: %s', e)
[email protected]04d119d2012-10-17 22:41:531067
Sergiy Byelozyorova68d82c2018-03-21 16:20:561068 my_activity.show_progress('\n')
1069
Vadim Bendebury8de38002018-05-15 02:02:551070 my_activity.print_access_errors()
1071
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:471072 output_file = None
1073 try:
1074 if options.output:
1075 output_file = open(options.output, 'w')
1076 logging.info('Printing output to "%s"', options.output)
1077 sys.stdout = output_file
1078 except (IOError, OSError) as e:
Varun Khanejad9f97bc2017-08-02 19:55:011079 logging.error('Unable to write output: %s', e)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:471080 else:
1081 if options.json:
1082 my_activity.dump_json()
1083 else:
Sergiy Byelozyorov544b7442018-03-16 20:44:581084 if options.changes:
1085 my_activity.print_changes()
1086 if options.reviews:
1087 my_activity.print_reviews()
1088 if options.issues:
1089 my_activity.print_issues()
1090 if options.changes_by_issue:
1091 my_activity.print_changes_by_issue(
1092 options.skip_own_issues_without_changes)
Nicolas Dossou-gbetee5deedf2017-03-15 16:26:471093 finally:
1094 if output_file:
1095 logging.info('Done printing to file.')
1096 sys.stdout = sys.__stdout__
1097 output_file.close()
1098
[email protected]04d119d2012-10-17 22:41:531099 return 0
1100
1101
1102if __name__ == '__main__':
[email protected]832d51e2015-05-27 12:52:511103 # Fix encoding to support non-ascii issue titles.
1104 fix_encoding.fix_encoding()
1105
[email protected]013731e2015-02-26 18:28:431106 try:
1107 sys.exit(main())
1108 except KeyboardInterrupt:
1109 sys.stderr.write('interrupted\n')
1110 sys.exit(1)