| OLD | NEW | 
|---|
| (Empty) |  | 
|  | 1 #!/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 | 
|  | 8 Example: | 
|  | 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. | 
|  | 14 """ | 
|  | 15 | 
|  | 16 # These services typically only provide a created time and a last modified time | 
|  | 17 # for each item for general queries. This is not enough to determine if there | 
|  | 18 # was activity in a given time period. So, we first query for all things created | 
|  | 19 # before end and modified after begin. Then, we get the details of each item and | 
|  | 20 # check those details to determine if there was activity in the given period. | 
|  | 21 # This means that query time scales mostly with (today() - begin). | 
|  | 22 | 
|  | 23 import cookielib | 
|  | 24 import datetime | 
|  | 25 from datetime import datetime | 
|  | 26 from datetime import timedelta | 
|  | 27 from functools import partial | 
|  | 28 import json | 
|  | 29 import optparse | 
|  | 30 import os | 
|  | 31 import subprocess | 
|  | 32 import sys | 
|  | 33 import urllib | 
|  | 34 import urllib2 | 
|  | 35 | 
|  | 36 import rietveld | 
|  | 37 from third_party import upload | 
|  | 38 | 
|  | 39 try: | 
|  | 40   from dateutil.relativedelta import relativedelta # pylint: disable=F0401 | 
|  | 41 except ImportError: | 
|  | 42   print 'python-dateutil package required' | 
|  | 43   exit(1) | 
|  | 44 | 
|  | 45 # python-keyring provides easy access to the system keyring. | 
|  | 46 try: | 
|  | 47   import keyring  # pylint: disable=W0611,F0401 | 
|  | 48 except ImportError: | 
|  | 49   print 'Consider installing python-keyring' | 
|  | 50 | 
|  | 51 | 
|  | 52 rietveld_instances = [ | 
|  | 53   { | 
|  | 54     'url': 'codereview.chromium.org', | 
|  | 55     'shorturl': 'crrev.com', | 
|  | 56     'supports_owner_modified_query': True, | 
|  | 57     'requires_auth': False, | 
|  | 58     'email_domain': 'chromium.org', | 
|  | 59   }, | 
|  | 60   { | 
|  | 61     'url': 'chromereviews.googleplex.com', | 
|  | 62     'shorturl': 'go/chromerev', | 
|  | 63     'supports_owner_modified_query': True, | 
|  | 64     'requires_auth': True, | 
|  | 65     'email_domain': 'google.com', | 
|  | 66   }, | 
|  | 67   { | 
|  | 68     'url': 'codereview.appspot.com', | 
|  | 69     'supports_owner_modified_query': True, | 
|  | 70     'requires_auth': False, | 
|  | 71     'email_domain': 'chromium.org', | 
|  | 72   }, | 
|  | 73   { | 
|  | 74     'url': 'breakpad.appspot.com', | 
|  | 75     'supports_owner_modified_query': False, | 
|  | 76     'requires_auth': False, | 
|  | 77     'email_domain': 'chromium.org', | 
|  | 78   }, | 
|  | 79 ] | 
|  | 80 | 
|  | 81 gerrit_instances = [ | 
|  | 82   { | 
|  | 83     'url': 'gerrit.chromium.org', | 
|  | 84     'port': 29418, | 
|  | 85   }, | 
|  | 86   { | 
|  | 87     'url': 'gerrit-int.chromium.org', | 
|  | 88     'port': 29419, | 
|  | 89   }, | 
|  | 90 ] | 
|  | 91 | 
|  | 92 google_code_projects = [ | 
|  | 93   { | 
|  | 94     'name': 'chromium', | 
|  | 95     'shorturl': 'crbug.com', | 
|  | 96   }, | 
|  | 97   { | 
|  | 98     'name': 'chromium-os', | 
|  | 99   }, | 
|  | 100   { | 
|  | 101     'name': 'chrome-os-partner', | 
|  | 102   }, | 
|  | 103   { | 
|  | 104     'name': 'google-breakpad', | 
|  | 105   }, | 
|  | 106   { | 
|  | 107     'name': 'gyp', | 
|  | 108   } | 
|  | 109 ] | 
|  | 110 | 
|  | 111 | 
|  | 112 # Uses ClientLogin to authenticate the user for Google Code issue trackers. | 
|  | 113 def get_auth_token(email): | 
|  | 114   error = Exception() | 
|  | 115   for _ in xrange(3): | 
|  | 116     email, password = ( | 
|  | 117         upload.KeyringCreds('code.google.com', 'google.com', email) | 
|  | 118         .GetUserCredentials()) | 
|  | 119     url = 'https://www.google.com/accounts/ClientLogin' | 
|  | 120     data = urllib.urlencode({ | 
|  | 121         'Email': email, | 
|  | 122         'Passwd': password, | 
|  | 123         'service': 'code', | 
|  | 124         'source': 'chrome-my-activity', | 
|  | 125         'accountType': 'GOOGLE', | 
|  | 126     }) | 
|  | 127     req = urllib2.Request(url, data=data, headers={'Accept': 'text/plain'}) | 
|  | 128     try: | 
|  | 129       response = urllib2.urlopen(req) | 
|  | 130       response_body = response.read() | 
|  | 131       response_dict = dict(x.split('=') | 
|  | 132                            for x in response_body.split('\n') if x) | 
|  | 133       return response_dict['Auth'] | 
|  | 134     except urllib2.HTTPError, e: | 
|  | 135       error = e | 
|  | 136 | 
|  | 137   raise error | 
|  | 138 | 
|  | 139 | 
|  | 140 def username(email): | 
|  | 141   """Keeps the username of an email address.""" | 
|  | 142   return email and email.split('@', 1)[0] | 
|  | 143 | 
|  | 144 | 
|  | 145 def get_quarter_of(date): | 
|  | 146   begin = date - relativedelta(months=(date.month % 3) - 1, days=(date.day - 1)) | 
|  | 147   return begin, begin + relativedelta(months=3) | 
|  | 148 | 
|  | 149 | 
|  | 150 def get_year_of(date): | 
|  | 151   begin = date - relativedelta(months=(date.month - 1), days=(date.day - 1)) | 
|  | 152   return begin, begin + relativedelta(years=1) | 
|  | 153 | 
|  | 154 | 
|  | 155 def get_week_of(date): | 
|  | 156   begin = date - timedelta(days=date.weekday()) | 
|  | 157   return begin, begin + timedelta(days=7) | 
|  | 158 | 
|  | 159 | 
|  | 160 def get_yes_or_no(msg): | 
|  | 161   while True: | 
|  | 162     response = raw_input(msg + ' yes/no [no] ') | 
|  | 163     if response == 'y' or response == 'yes': | 
|  | 164       return True | 
|  | 165     elif not response or response == 'n' or response == 'no': | 
|  | 166       return False | 
|  | 167 | 
|  | 168 | 
|  | 169 def datetime_from_rietveld(date_string): | 
|  | 170   return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f') | 
|  | 171 | 
|  | 172 | 
|  | 173 def datetime_from_google_code(date_string): | 
|  | 174   return datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S.%fZ') | 
|  | 175 | 
|  | 176 | 
|  | 177 class MyActivity(object): | 
|  | 178   def __init__(self, options): | 
|  | 179     self.options = options | 
|  | 180     self.modified_after = options.begin | 
|  | 181     self.modified_before = options.end | 
|  | 182     self.user = options.user | 
|  | 183     self.changes = [] | 
|  | 184     self.reviews = [] | 
|  | 185     self.issues = [] | 
|  | 186     self.check_cookies() | 
|  | 187     self.google_code_auth_token = None | 
|  | 188 | 
|  | 189   # Check the codereview cookie jar to determine which Rietveld instances to | 
|  | 190   # authenticate to. | 
|  | 191   def check_cookies(self): | 
|  | 192     cookie_file = os.path.expanduser('~/.codereview_upload_cookies') | 
|  | 193     cookie_jar = cookielib.MozillaCookieJar(cookie_file) | 
|  | 194     if not os.path.exists(cookie_file): | 
|  | 195       exit(1) | 
|  | 196 | 
|  | 197     try: | 
|  | 198       cookie_jar.load() | 
|  | 199       print 'Found cookie file: %s' % cookie_file | 
|  | 200     except (cookielib.LoadError, IOError): | 
|  | 201       exit(1) | 
|  | 202 | 
|  | 203     filtered_instances = [] | 
|  | 204 | 
|  | 205     def has_cookie(instance): | 
|  | 206       for cookie in cookie_jar: | 
|  | 207         if cookie.name == 'SACSID' and cookie.domain == instance['url']: | 
|  | 208           return True | 
|  | 209       if self.options.auth: | 
|  | 210         return get_yes_or_no('No cookie found for %s. Authorize for this ' | 
|  | 211                              'instance? (may require application-specific ' | 
|  | 212                              'password)' % instance['url']) | 
|  | 213       filtered_instances.append(instance) | 
|  | 214       return False | 
|  | 215 | 
|  | 216     for instance in rietveld_instances: | 
|  | 217       instance['auth'] = has_cookie(instance) | 
|  | 218 | 
|  | 219     if filtered_instances: | 
|  | 220       print ('No cookie found for the following Rietveld instance%s:' % | 
|  | 221              ('s' if len(filtered_instances) > 1 else '')) | 
|  | 222       for instance in filtered_instances: | 
|  | 223         print '\t' + instance['url'] | 
|  | 224       print 'Use --auth if you would like to authenticate to them.\n' | 
|  | 225 | 
|  | 226   def rietveld_search(self, instance, owner=None, reviewer=None): | 
|  | 227     if instance['requires_auth'] and not instance['auth']: | 
|  | 228       return [] | 
|  | 229 | 
|  | 230 | 
|  | 231     email = None if instance['auth'] else '' | 
|  | 232     remote = rietveld.Rietveld('https://' + instance['url'], email, None) | 
|  | 233 | 
|  | 234     # See def search() in rietveld.py to see all the filters you can use. | 
|  | 235     query_modified_after = None | 
|  | 236 | 
|  | 237     if instance['supports_owner_modified_query']: | 
|  | 238       query_modified_after = self.modified_after.strftime('%Y-%m-%d') | 
|  | 239 | 
|  | 240     # Rietveld does not allow search by both created_before and modified_after. | 
|  | 241     # (And some instances don't allow search by both owner and modified_after) | 
|  | 242     owner_email = None | 
|  | 243     reviewer_email = None | 
|  | 244     if owner: | 
|  | 245       owner_email = owner + '@' + instance['email_domain'] | 
|  | 246     if reviewer: | 
|  | 247       reviewer_email = reviewer + '@' + instance['email_domain'] | 
|  | 248     issues = remote.search( | 
|  | 249         owner=owner_email, | 
|  | 250         reviewer=reviewer_email, | 
|  | 251         modified_after=query_modified_after, | 
|  | 252         with_messages=True) | 
|  | 253 | 
|  | 254     issues = filter( | 
|  | 255         lambda i: (datetime_from_rietveld(i['created']) < self.modified_before), | 
|  | 256         issues) | 
|  | 257     issues = filter( | 
|  | 258         lambda i: (datetime_from_rietveld(i['modified']) > self.modified_after), | 
|  | 259         issues) | 
|  | 260 | 
|  | 261     should_filter_by_user = True | 
|  | 262     issues = map(partial(self.process_rietveld_issue, instance), issues) | 
|  | 263     issues = filter( | 
|  | 264         partial(self.filter_issue, should_filter_by_user=should_filter_by_user), | 
|  | 265         issues) | 
|  | 266     issues = sorted(issues, key=lambda i: i['modified'], reverse=True) | 
|  | 267 | 
|  | 268     return issues | 
|  | 269 | 
|  | 270   def process_rietveld_issue(self, instance, issue): | 
|  | 271     ret = {} | 
|  | 272     ret['owner'] = issue['owner_email'] | 
|  | 273     ret['author'] = ret['owner'] | 
|  | 274 | 
|  | 275     ret['reviewers'] = set(username(r) for r in issue['reviewers']) | 
|  | 276 | 
|  | 277     shorturl = instance['url'] | 
|  | 278     if 'shorturl' in instance: | 
|  | 279       shorturl = instance['shorturl'] | 
|  | 280 | 
|  | 281     ret['review_url'] = 'http://%s/%d' % (shorturl, issue['issue']) | 
|  | 282     ret['header'] = issue['description'].split('\n')[0] | 
|  | 283 | 
|  | 284     ret['modified'] = datetime_from_rietveld(issue['modified']) | 
|  | 285     ret['created'] = datetime_from_rietveld(issue['created']) | 
|  | 286     ret['replies'] = self.process_rietveld_replies(issue['messages']) | 
|  | 287 | 
|  | 288     return ret | 
|  | 289 | 
|  | 290   @staticmethod | 
|  | 291   def process_rietveld_replies(replies): | 
|  | 292     ret = [] | 
|  | 293     for reply in replies: | 
|  | 294       r = {} | 
|  | 295       r['author'] = reply['sender'] | 
|  | 296       r['created'] = datetime_from_rietveld(reply['date']) | 
|  | 297       r['content'] = '' | 
|  | 298       ret.append(r) | 
|  | 299     return ret | 
|  | 300 | 
|  | 301   def gerrit_search(self, instance, owner=None, reviewer=None): | 
|  | 302     max_age = datetime.today() - self.modified_after | 
|  | 303     max_age = max_age.days * 24 * 3600 + max_age.seconds | 
|  | 304 | 
|  | 305     # See https://review.openstack.org/Documentation/cmd-query.html | 
|  | 306     # Gerrit doesn't allow filtering by created time, only modified time. | 
|  | 307     user_filter = 'owner:%s' % owner if owner else 'reviewer:%s' % reviewer | 
|  | 308     gquery_cmd = ['ssh', '-p', str(instance['port']), instance['url'], | 
|  | 309                   'gerrit', 'query', | 
|  | 310                   '--format', 'JSON', | 
|  | 311                   '--comments', | 
|  | 312                   '--', | 
|  | 313                   '-age:%ss' % str(max_age), | 
|  | 314                   user_filter] | 
|  | 315     [stdout, _] = subprocess.Popen(gquery_cmd, stdout=subprocess.PIPE, | 
|  | 316                                    stderr=subprocess.PIPE).communicate() | 
|  | 317     issues = str(stdout).split('\n')[:-2] | 
|  | 318     issues = map(json.loads, issues) | 
|  | 319 | 
|  | 320     # TODO(cjhopman): should we filter abandoned changes? | 
|  | 321     issues = map(self.process_gerrit_issue, issues) | 
|  | 322     issues = filter(self.filter_issue, issues) | 
|  | 323     issues = sorted(issues, key=lambda i: i['modified'], reverse=True) | 
|  | 324 | 
|  | 325     return issues | 
|  | 326 | 
|  | 327   def process_gerrit_issue(self, issue): | 
|  | 328     ret = {} | 
|  | 329     ret['review_url'] = issue['url'] | 
|  | 330     ret['header'] = issue['subject'] | 
|  | 331     ret['owner'] = issue['owner']['email'] | 
|  | 332     ret['author'] = ret['owner'] | 
|  | 333     ret['created'] = datetime.fromtimestamp(issue['createdOn']) | 
|  | 334     ret['modified'] = datetime.fromtimestamp(issue['lastUpdated']) | 
|  | 335     if 'comments' in issue: | 
|  | 336       ret['replies'] = self.process_gerrit_issue_replies(issue['comments']) | 
|  | 337     else: | 
|  | 338       ret['replies'] = [] | 
|  | 339     return ret | 
|  | 340 | 
|  | 341   @staticmethod | 
|  | 342   def process_gerrit_issue_replies(replies): | 
|  | 343     ret = [] | 
|  | 344     replies = filter(lambda r: 'email' in r['reviewer'], replies) | 
|  | 345     for reply in replies: | 
|  | 346       r = {} | 
|  | 347       r['author'] = reply['reviewer']['email'] | 
|  | 348       r['created'] = datetime.fromtimestamp(reply['timestamp']) | 
|  | 349       r['content'] = '' | 
|  | 350       ret.append(r) | 
|  | 351     return ret | 
|  | 352 | 
|  | 353   def google_code_issue_search(self, instance): | 
|  | 354     time_format = '%Y-%m-%dT%T' | 
|  | 355     # See http://code.google.com/p/support/wiki/IssueTrackerAPI | 
|  | 356     # q=<owner>@chromium.org does a full text search for <owner>@chromium.org. | 
|  | 357     # This will accept the issue if owner is the owner or in the cc list. Might | 
|  | 358     # have some false positives, though. | 
|  | 359 | 
|  | 360     # Don't filter normally on modified_before because it can filter out things | 
|  | 361     # that were modified in the time period and then modified again after it. | 
|  | 362     gcode_url = ('https://code.google.com/feeds/issues/p/%s/issues/full' % | 
|  | 363                  instance['name']) | 
|  | 364 | 
|  | 365     gcode_data = urllib.urlencode({ | 
|  | 366         'alt': 'json', | 
|  | 367         'max-results': '100000', | 
|  | 368         'q': '%s' % self.user, | 
|  | 369         'published-max': self.modified_before.strftime(time_format), | 
|  | 370         'updated-min': self.modified_after.strftime(time_format), | 
|  | 371     }) | 
|  | 372 | 
|  | 373     opener = urllib2.build_opener() | 
|  | 374     opener.addheaders = [('Authorization', 'GoogleLogin auth=%s' % | 
|  | 375                           self.google_code_auth_token)] | 
|  | 376     gcode_get = opener.open(gcode_url + '?' + gcode_data) | 
|  | 377     gcode_json = json.load(gcode_get) | 
|  | 378     gcode_get.close() | 
|  | 379 | 
|  | 380     if 'entry' not in gcode_json['feed']: | 
|  | 381       return [] | 
|  | 382 | 
|  | 383     issues = gcode_json['feed']['entry'] | 
|  | 384     issues = map(partial(self.process_google_code_issue, instance), issues) | 
|  | 385     issues = filter(self.filter_issue, issues) | 
|  | 386     issues = sorted(issues, key=lambda i: i['modified'], reverse=True) | 
|  | 387     return issues | 
|  | 388 | 
|  | 389   def process_google_code_issue(self, project, issue): | 
|  | 390     ret = {} | 
|  | 391     ret['created'] = datetime_from_google_code(issue['published']['$t']) | 
|  | 392     ret['modified'] = datetime_from_google_code(issue['updated']['$t']) | 
|  | 393 | 
|  | 394     ret['owner'] = '' | 
|  | 395     if 'issues:owner' in issue: | 
|  | 396       ret['owner'] = issue['issues:owner'][0]['issues:username'][0]['$t'] | 
|  | 397     ret['author'] = issue['author'][0]['name']['$t'] | 
|  | 398 | 
|  | 399     if 'shorturl' in project: | 
|  | 400       issue_id = issue['id']['$t'] | 
|  | 401       issue_id = issue_id[issue_id.rfind('/') + 1:] | 
|  | 402       ret['url'] = 'http://%s/%d' % (project['shorturl'], int(issue_id)) | 
|  | 403     else: | 
|  | 404       issue_url = issue['link'][1] | 
|  | 405       if issue_url['rel'] != 'alternate': | 
|  | 406         raise RuntimeError | 
|  | 407       ret['url'] = issue_url['href'] | 
|  | 408     ret['header'] = issue['title']['$t'] | 
|  | 409 | 
|  | 410     ret['replies'] = self.get_google_code_issue_replies(issue) | 
|  | 411     return ret | 
|  | 412 | 
|  | 413   def get_google_code_issue_replies(self, issue): | 
|  | 414     """Get all the comments on the issue.""" | 
|  | 415     replies_url = issue['link'][0] | 
|  | 416     if replies_url['rel'] != 'replies': | 
|  | 417       raise RuntimeError | 
|  | 418 | 
|  | 419     replies_data = urllib.urlencode({ | 
|  | 420         'alt': 'json', | 
|  | 421         'fields': 'entry(published,author,content)', | 
|  | 422     }) | 
|  | 423 | 
|  | 424     opener = urllib2.build_opener() | 
|  | 425     opener.addheaders = [('Authorization', 'GoogleLogin auth=%s' % | 
|  | 426                           self.google_code_auth_token)] | 
|  | 427     try: | 
|  | 428       replies_get = opener.open(replies_url['href'] + '?' + replies_data) | 
|  | 429     except urllib2.HTTPError, _: | 
|  | 430       return [] | 
|  | 431 | 
|  | 432     replies_json = json.load(replies_get) | 
|  | 433     replies_get.close() | 
|  | 434     return self.process_google_code_issue_replies(replies_json) | 
|  | 435 | 
|  | 436   @staticmethod | 
|  | 437   def process_google_code_issue_replies(replies): | 
|  | 438     if 'entry' not in replies['feed']: | 
|  | 439       return [] | 
|  | 440 | 
|  | 441     ret = [] | 
|  | 442     for entry in replies['feed']['entry']: | 
|  | 443       e = {} | 
|  | 444       e['created'] = datetime_from_google_code(entry['published']['$t']) | 
|  | 445       e['content'] = entry['content']['$t'] | 
|  | 446       e['author'] = entry['author'][0]['name']['$t'] | 
|  | 447       ret.append(e) | 
|  | 448     return ret | 
|  | 449 | 
|  | 450   @staticmethod | 
|  | 451   def print_change(change): | 
|  | 452     print '%s %s' % ( | 
|  | 453         change['review_url'], | 
|  | 454         change['header'], | 
|  | 455         ) | 
|  | 456 | 
|  | 457   @staticmethod | 
|  | 458   def print_issue(issue): | 
|  | 459     print '%s %s' % ( | 
|  | 460         issue['url'], | 
|  | 461         issue['header'], | 
|  | 462         ) | 
|  | 463 | 
|  | 464   def filter_issue(self, issue, should_filter_by_user=True): | 
|  | 465     def maybe_filter_username(email): | 
|  | 466       return not should_filter_by_user or username(email) == self.user | 
|  | 467     if (maybe_filter_username(issue['author']) and | 
|  | 468         self.filter_modified(issue['created'])): | 
|  | 469       return True | 
|  | 470     if (maybe_filter_username(issue['owner']) and | 
|  | 471         (self.filter_modified(issue['created']) or | 
|  | 472          self.filter_modified(issue['modified']))): | 
|  | 473       return True | 
|  | 474     for reply in issue['replies']: | 
|  | 475       if self.filter_modified(reply['created']): | 
|  | 476         if not should_filter_by_user: | 
|  | 477           break | 
|  | 478         if (username(reply['author']) == self.user | 
|  | 479             or (self.user + '@') in reply['content']): | 
|  | 480           break | 
|  | 481     else: | 
|  | 482       return False | 
|  | 483     return True | 
|  | 484 | 
|  | 485   def filter_modified(self, modified): | 
|  | 486     return self.modified_after < modified and modified < self.modified_before | 
|  | 487 | 
|  | 488   def auth_for_changes(self): | 
|  | 489     #TODO(cjhopman): Move authentication check for getting changes here. | 
|  | 490     pass | 
|  | 491 | 
|  | 492   def auth_for_reviews(self): | 
|  | 493     # Reviews use all the same instances as changes so no authentication is | 
|  | 494     # required. | 
|  | 495     pass | 
|  | 496 | 
|  | 497   def auth_for_issues(self): | 
|  | 498     self.google_code_auth_token = ( | 
|  | 499         get_auth_token(self.options.local_user + '@chromium.org')) | 
|  | 500 | 
|  | 501   def get_changes(self): | 
|  | 502     for instance in rietveld_instances: | 
|  | 503       self.changes += self.rietveld_search(instance, owner=self.user) | 
|  | 504 | 
|  | 505     for instance in gerrit_instances: | 
|  | 506       self.changes += self.gerrit_search(instance, owner=self.user) | 
|  | 507 | 
|  | 508   def print_changes(self): | 
|  | 509     if self.changes: | 
|  | 510       print '\nChanges:' | 
|  | 511       for change in self.changes: | 
|  | 512         self.print_change(change) | 
|  | 513 | 
|  | 514   def get_reviews(self): | 
|  | 515     for instance in rietveld_instances: | 
|  | 516       self.reviews += self.rietveld_search(instance, reviewer=self.user) | 
|  | 517 | 
|  | 518     for instance in gerrit_instances: | 
|  | 519       reviews = self.gerrit_search(instance, reviewer=self.user) | 
|  | 520       reviews = filter(lambda r: not username(r['owner']) == self.user, reviews) | 
|  | 521       self.reviews += reviews | 
|  | 522 | 
|  | 523   def print_reviews(self): | 
|  | 524     if self.reviews: | 
|  | 525       print '\nReviews:' | 
|  | 526       for review in self.reviews: | 
|  | 527         self.print_change(review) | 
|  | 528 | 
|  | 529   def get_issues(self): | 
|  | 530     for project in google_code_projects: | 
|  | 531       self.issues += self.google_code_issue_search(project) | 
|  | 532 | 
|  | 533   def print_issues(self): | 
|  | 534     if self.issues: | 
|  | 535       print '\nIssues:' | 
|  | 536       for c in self.issues: | 
|  | 537         self.print_issue(c) | 
|  | 538 | 
|  | 539   def print_activity(self): | 
|  | 540     self.print_changes() | 
|  | 541     self.print_reviews() | 
|  | 542     self.print_issues() | 
|  | 543 | 
|  | 544 | 
|  | 545 def main(): | 
|  | 546   # Silence upload.py. | 
|  | 547   rietveld.upload.verbosity = 0 | 
|  | 548 | 
|  | 549   parser = optparse.OptionParser(description=sys.modules[__name__].__doc__) | 
|  | 550   parser.add_option( | 
|  | 551       '-u', '--user', metavar='<email>', | 
|  | 552       default=os.environ.get('USER'), | 
|  | 553       help='Filter on user, default=%default') | 
|  | 554   parser.add_option( | 
|  | 555       '-b', '--begin', metavar='<date>', | 
|  | 556       help='Filter issues created after the date') | 
|  | 557   parser.add_option( | 
|  | 558       '-e', '--end', metavar='<date>', | 
|  | 559       help='Filter issues created before the date') | 
|  | 560   quarter_begin, quarter_end = get_quarter_of(datetime.today() - | 
|  | 561                                               relativedelta(months=2)) | 
|  | 562   parser.add_option( | 
|  | 563       '-Q', '--last_quarter', action='store_true', | 
|  | 564       help='Use last quarter\'s dates, e.g. %s to %s' % ( | 
|  | 565         quarter_begin.strftime('%Y-%m-%d'), quarter_end.strftime('%Y-%m-%d'))) | 
|  | 566   parser.add_option( | 
|  | 567       '-Y', '--this_year', action='store_true', | 
|  | 568       help='Use this year\'s dates') | 
|  | 569   parser.add_option( | 
|  | 570       '-w', '--week_of', metavar='<date>', | 
|  | 571       help='Show issues for week of the date') | 
|  | 572   parser.add_option( | 
|  | 573       '-a', '--auth', | 
|  | 574       action='store_true', | 
|  | 575       help='Ask to authenticate for instances with no auth cookie') | 
|  | 576 | 
|  | 577   group = optparse.OptionGroup(parser, 'Activity Types', | 
|  | 578                                'By default, all activity will be looked up and ' | 
|  | 579                                'printed. If any of these are specified, only ' | 
|  | 580                                'those specified will be searched.') | 
|  | 581   group.add_option( | 
|  | 582       '-c', '--changes', | 
|  | 583       action='store_true', | 
|  | 584       help='Show changes.') | 
|  | 585   group.add_option( | 
|  | 586       '-i', '--issues', | 
|  | 587       action='store_true', | 
|  | 588       help='Show issues.') | 
|  | 589   group.add_option( | 
|  | 590       '-r', '--reviews', | 
|  | 591       action='store_true', | 
|  | 592       help='Show reviews.') | 
|  | 593   parser.add_option_group(group) | 
|  | 594 | 
|  | 595   # Remove description formatting | 
|  | 596   parser.format_description = ( | 
|  | 597       lambda _: parser.description)  # pylint: disable=E1101 | 
|  | 598 | 
|  | 599   options, args = parser.parse_args() | 
|  | 600   options.local_user = os.environ.get('USER') | 
|  | 601   if args: | 
|  | 602     parser.error('Args unsupported') | 
|  | 603   if not options.user: | 
|  | 604     parser.error('USER is not set, please use -u') | 
|  | 605 | 
|  | 606   options.user = username(options.user) | 
|  | 607 | 
|  | 608   if not options.begin: | 
|  | 609     if options.last_quarter: | 
|  | 610       begin, end = quarter_begin, quarter_end | 
|  | 611     elif options.this_year: | 
|  | 612       begin, end = get_year_of(datetime.today()) | 
|  | 613     elif options.week_of: | 
|  | 614       begin, end = (get_week_of(datetime.strptime(options.week_of, '%m/%d/%y'))) | 
|  | 615     else: | 
|  | 616       begin, end = (get_week_of(datetime.today() - timedelta(days=1))) | 
|  | 617   else: | 
|  | 618     begin = datetime.strptime(options.begin, '%m/%d/%y') | 
|  | 619     if options.end: | 
|  | 620       end = datetime.strptime(options.end, '%m/%d/%y') | 
|  | 621     else: | 
|  | 622       end = datetime.today() | 
|  | 623   options.begin, options.end = begin, end | 
|  | 624 | 
|  | 625   print 'Searching for activity by %s' % options.user | 
|  | 626   print 'Using range %s to %s' % (options.begin, options.end) | 
|  | 627 | 
|  | 628   my_activity = MyActivity(options) | 
|  | 629 | 
|  | 630   if not (options.changes or options.reviews or options.issues): | 
|  | 631     options.changes = True | 
|  | 632     options.issues = True | 
|  | 633     options.reviews = True | 
|  | 634 | 
|  | 635   # First do any required authentication so none of the user interaction has to | 
|  | 636   # wait for actual work. | 
|  | 637   if options.changes: | 
|  | 638     my_activity.auth_for_changes() | 
|  | 639   if options.reviews: | 
|  | 640     my_activity.auth_for_reviews() | 
|  | 641   if options.issues: | 
|  | 642     my_activity.auth_for_issues() | 
|  | 643 | 
|  | 644   print 'Looking up activity.....' | 
|  | 645 | 
|  | 646   if options.changes: | 
|  | 647     my_activity.get_changes() | 
|  | 648   if options.reviews: | 
|  | 649     my_activity.get_reviews() | 
|  | 650   if options.issues: | 
|  | 651     my_activity.get_issues() | 
|  | 652 | 
|  | 653   print '\n\n\n' | 
|  | 654 | 
|  | 655   my_activity.print_changes() | 
|  | 656   my_activity.print_reviews() | 
|  | 657   my_activity.print_issues() | 
|  | 658   return 0 | 
|  | 659 | 
|  | 660 | 
|  | 661 if __name__ == '__main__': | 
|  | 662   sys.exit(main()) | 
| OLD | NEW | 
|---|