| 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 |