| 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 """Count commits by the commit queue.""" |  | 
| 6 |  | 
| 7 import datetime |  | 
| 8 import json |  | 
| 9 import logging |  | 
| 10 import optparse |  | 
| 11 import os |  | 
| 12 import re |  | 
| 13 import sys |  | 
| 14 from xml.etree import ElementTree |  | 
| 15 |  | 
| 16 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |  | 
| 17 |  | 
| 18 import find_depot_tools  # pylint: disable=W0611 |  | 
| 19 import subprocess2 |  | 
| 20 |  | 
| 21 |  | 
| 22 def log(repo, args): |  | 
| 23   """If extra is True, grab one revision before and one after.""" |  | 
| 24   args = args or [] |  | 
| 25   out = subprocess2.check_output( |  | 
| 26       ['svn', 'log', '--with-all-revprops', '--xml', repo] + args) |  | 
| 27   data = {} |  | 
| 28   for logentry in ElementTree.XML(out).findall('logentry'): |  | 
| 29     date_str = logentry.find('date').text |  | 
| 30     date = datetime.datetime(*map(int, re.split('[^\d]', date_str)[:-1])) |  | 
| 31     entry = { |  | 
| 32         'author': logentry.find('author').text, |  | 
| 33         'date': date, |  | 
| 34         'msg': logentry.find('msg').text, |  | 
| 35         'revprops': {}, |  | 
| 36         'commit-bot': False, |  | 
| 37     } |  | 
| 38     revprops = logentry.find('revprops') |  | 
| 39     if revprops is not None: |  | 
| 40       for revprop in revprops.findall('property'): |  | 
| 41         entry['revprops'][revprop.attrib['name']] = revprop.text |  | 
| 42         if revprop.attrib['name'] == 'commit-bot': |  | 
| 43           entry['commit-bot'] = True |  | 
| 44     data[logentry.attrib['revision']] = entry |  | 
| 45   return data |  | 
| 46 |  | 
| 47 |  | 
| 48 def log_dates(repo, start_date, days): |  | 
| 49   """Formats dates so 'svn log' does the right thing. Fetches everything in UTC. |  | 
| 50   """ |  | 
| 51   # http://svnbook.red-bean.com/nightly/en/svn-book.html#svn.tour.revs.dates |  | 
| 52   if not days: |  | 
| 53     end_inclusive = datetime.date.today() |  | 
| 54   else: |  | 
| 55     end_inclusive = start_date + datetime.timedelta(days=days) |  | 
| 56   actual_days = (end_inclusive - start_date).days |  | 
| 57   print('Getting data from %s for %s days' % (start_date, actual_days)) |  | 
| 58   range_str = ( |  | 
| 59       '{%s 00:00:00 +0000}:{%s 00:00:00 +0000}' % (start_date, end_inclusive)) |  | 
| 60   data = log(repo, ['-r', range_str]) |  | 
| 61   # Strip off everything outside the range. |  | 
| 62   start_date_time = datetime.datetime(*start_date.timetuple()[:6]) |  | 
| 63   if data: |  | 
| 64     first = sorted(data.keys())[0] |  | 
| 65     if data[first]['date'] < start_date_time: |  | 
| 66       del data[first] |  | 
| 67   # Strip the commit message to save space. |  | 
| 68   for item in data.itervalues(): |  | 
| 69     del item['msg'] |  | 
| 70   return data |  | 
| 71 |  | 
| 72 |  | 
| 73 def monday_last_week(): |  | 
| 74   """Returns Monday in 'date' object.""" |  | 
| 75   today = datetime.date.today() |  | 
| 76   last_week = today - datetime.timedelta(days=7) |  | 
| 77   return last_week - datetime.timedelta(days=(last_week.isoweekday() - 1)) |  | 
| 78 |  | 
| 79 |  | 
| 80 class JSONEncoder(json.JSONEncoder): |  | 
| 81   def default(self, o):  # pylint: disable=E0202 |  | 
| 82     if isinstance(o, datetime.datetime): |  | 
| 83       return str(o) |  | 
| 84     return super(JSONEncoder, self) |  | 
| 85 |  | 
| 86 |  | 
| 87 def print_aligned(zipped_list): |  | 
| 88   max_len = max(len(i[0]) for i in zipped_list) |  | 
| 89   for author, count in zipped_list: |  | 
| 90     print('%*s: %d' % (max_len, author, count)) |  | 
| 91 |  | 
| 92 |  | 
| 93 def print_data(log_data, stats_only, top): |  | 
| 94   # Calculate stats. |  | 
| 95   num_commit_bot = len([True for v in log_data.itervalues() if v['commit-bot']]) |  | 
| 96   num_total_commits = len(log_data) |  | 
| 97   pourcent = 0. |  | 
| 98   if num_total_commits: |  | 
| 99     pourcent = float(num_commit_bot) * 100. / float(num_total_commits) |  | 
| 100   users = {} |  | 
| 101   for i in log_data.itervalues(): |  | 
| 102     if i['commit-bot']: |  | 
| 103       users.setdefault(i['author'], 0) |  | 
| 104       users[i['author']] += 1 |  | 
| 105 |  | 
| 106   if not stats_only: |  | 
| 107     max_author_len = max(len(i['author']) for i in log_data.itervalues()) |  | 
| 108     for revision in sorted(log_data.keys()): |  | 
| 109       entry = log_data[revision] |  | 
| 110       commit_bot = ' ' |  | 
| 111       if entry['commit-bot']: |  | 
| 112         commit_bot = 'c' |  | 
| 113       print('%s  %s  %s  %*s' % ( |  | 
| 114         ('r%s' % revision).rjust(6), |  | 
| 115         commit_bot, |  | 
| 116         entry['date'].strftime('%Y-%m-%d %H:%M UTC'), |  | 
| 117         max_author_len, |  | 
| 118         entry['author'])) |  | 
| 119     print('') |  | 
| 120 |  | 
| 121   if top: |  | 
| 122     top_users = sorted( |  | 
| 123         users.iteritems(), key=lambda x: x[1], reverse=True)[:top] |  | 
| 124     top_commits = sum(x[1] for x in top_users) |  | 
| 125     p_u = 100. * len(top_users) / len(users) |  | 
| 126     p_c = 100. * top_commits / num_commit_bot |  | 
| 127     print( |  | 
| 128         'Top users:      %6d out of %6d total users  %6.2f%%' % |  | 
| 129         (len(top_users), len(users), p_u)) |  | 
| 130     print( |  | 
| 131         '  Committed     %6d out of %6d CQ\'ed commits %5.2f%%' % |  | 
| 132         (top_commits, num_commit_bot, p_c)) |  | 
| 133     if not stats_only: |  | 
| 134       print_aligned(top_users) |  | 
| 135 |  | 
| 136   non_committers = sorted( |  | 
| 137         ( (u, c) for u, c in users.iteritems() |  | 
| 138           if not u.endswith('@chromium.org')), |  | 
| 139         key=lambda x: x[1], reverse=True) |  | 
| 140   if non_committers: |  | 
| 141     print('') |  | 
| 142     n_c_commits = sum(x[1] for x in non_committers) |  | 
| 143     p_u = 100. * len(non_committers) / len(users) |  | 
| 144     p_c = 100. * n_c_commits / num_commit_bot |  | 
| 145     print( |  | 
| 146         'Non-committers: %6d out of %6d total users  %6.2f%%' % |  | 
| 147         (len(non_committers), len(users), p_u)) |  | 
| 148     print( |  | 
| 149         '  Committed     %6d out of %6d CQ\'ed commits %5.2f%%' % |  | 
| 150         (n_c_commits, num_commit_bot, p_c)) |  | 
| 151     print('') |  | 
| 152     print('Top domains') |  | 
| 153     domains = {} |  | 
| 154     for user, count in non_committers: |  | 
| 155       domain = user.split('@', 1)[1] |  | 
| 156       domains.setdefault(domain, 0) |  | 
| 157       domains[domain] += count |  | 
| 158     domains_stats = sorted( |  | 
| 159         ((k, v) for k, v in domains.iteritems()), |  | 
| 160         key=lambda x: x[1], reverse=True) |  | 
| 161     print_aligned(domains_stats) |  | 
| 162     if not stats_only: |  | 
| 163       print_aligned(non_committers) |  | 
| 164 |  | 
| 165   print('') |  | 
| 166   print('Total commits:               %6d' % num_total_commits) |  | 
| 167   print( |  | 
| 168       'Total commits by commit bot: %6d (%6.1f%%)' % (num_commit_bot, pourcent)) |  | 
| 169 |  | 
| 170 |  | 
| 171 def main(): |  | 
| 172   parser = optparse.OptionParser( |  | 
| 173       description=sys.modules['__main__'].__doc__) |  | 
| 174   parser.add_option('-v', '--verbose', action='store_true') |  | 
| 175   parser.add_option( |  | 
| 176     '-r', '--repo', default='http://src.chromium.org/svn/trunk') |  | 
| 177   parser.add_option('-s', '--since', action='store') |  | 
| 178   parser.add_option('-d', '--days', type=int, default=7) |  | 
| 179   parser.add_option('--all', action='store_true', help='Get ALL the revisions!') |  | 
| 180   parser.add_option('--dump', help='Dump json in file') |  | 
| 181   parser.add_option('--read', help='Read the data from a file') |  | 
| 182   parser.add_option('-o', '--stats_only', action='store_true') |  | 
| 183   parser.add_option('--top', default=20, type='int') |  | 
| 184   options, args = parser.parse_args(None) |  | 
| 185   if args: |  | 
| 186     parser.error('Unsupported args: %s' % args) |  | 
| 187   logging.basicConfig( |  | 
| 188       level=(logging.DEBUG if options.verbose else logging.ERROR)) |  | 
| 189 |  | 
| 190   # By default, grab stats for last week. |  | 
| 191   if not options.since: |  | 
| 192     options.since = monday_last_week() |  | 
| 193   else: |  | 
| 194     options.since = datetime.date(*map(int, re.split('[^\d]', options.since))) |  | 
| 195 |  | 
| 196   if options.read: |  | 
| 197     if options.dump: |  | 
| 198       parser.error('Can\'t use --dump and --read simultaneously') |  | 
| 199     log_data = json.load(open(options.read, 'r')) |  | 
| 200     for entry in log_data.itervalues(): |  | 
| 201       # Convert strings like "2012-09-04 01:14:43.785581" to a datetime object. |  | 
| 202       entry['date'] = datetime.datetime.strptime( |  | 
| 203           entry['date'], '%Y-%m-%d %H:%M:%S.%f') |  | 
| 204   else: |  | 
| 205     if options.all: |  | 
| 206       log_data = log(options.repo, []) |  | 
| 207     else: |  | 
| 208       log_data = log_dates(options.repo, options.since, options.days) |  | 
| 209     if options.dump: |  | 
| 210       json.dump(log_data, open(options.dump, 'w'), cls=JSONEncoder) |  | 
| 211 |  | 
| 212   print_data(log_data, options.stats_only, options.top) |  | 
| 213   return 0 |  | 
| 214 |  | 
| 215 |  | 
| 216 if __name__ == '__main__': |  | 
| 217   sys.exit(main()) |  | 
| OLD | NEW | 
|---|