blob: a55485fe3a125b25b6fa6bb12964ffc26a644554 [file] [log] [blame]
[email protected]c73e5162011-09-21 23:16:121#!/usr/bin/env python
[email protected]40b76872012-03-21 01:07:442# Copyright (c) 2012 The Chromium Authors. All rights reserved.
[email protected]c73e5162011-09-21 23:16:123# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
[email protected]0bccb922011-09-22 19:22:226"""Get rietveld stats about the review you done, or forgot to do.
[email protected]c73e5162011-09-21 23:16:127
8Example:
[email protected]af52a502011-09-22 20:54:379 - my_reviews.py -r [email protected] -Q for stats for last quarter.
[email protected]c73e5162011-09-21 23:16:1210"""
11import datetime
[email protected]34dd5eb2011-09-23 21:11:1812import math
[email protected]c73e5162011-09-21 23:16:1213import optparse
14import os
15import sys
16
17import rietveld
18
19
[email protected]0bccb922011-09-22 19:22:2220def username(email):
[email protected]34dd5eb2011-09-23 21:11:1821 """Keeps the username of an email address."""
[email protected]0bccb922011-09-22 19:22:2222 return email.split('@', 1)[0]
[email protected]c73e5162011-09-21 23:16:1223
[email protected]0bccb922011-09-22 19:22:2224
[email protected]34dd5eb2011-09-23 21:11:1825def to_datetime(string):
26 """Load UTC time as a string into a datetime object."""
27 try:
28 # Format is 2011-07-05 01:26:12.084316
29 return datetime.datetime.strptime(
30 string.split('.', 1)[0], '%Y-%m-%d %H:%M:%S')
31 except ValueError:
32 return datetime.datetime.strptime(string, '%Y-%m-%d')
33
34
35def to_time(seconds):
36 """Convert a number of seconds into human readable compact string."""
37 prefix = ''
38 if seconds < 0:
39 prefix = '-'
40 seconds *= -1
41 minutes = math.floor(seconds / 60)
42 seconds -= minutes * 60
43 hours = math.floor(minutes / 60)
44 minutes -= hours * 60
45 days = math.floor(hours / 24)
46 hours -= days * 24
47 out = []
48 if days > 0:
49 out.append('%dd' % days)
50 if hours > 0 or days > 0:
51 out.append('%02dh' % hours)
52 if minutes > 0 or hours > 0 or days > 0:
53 out.append('%02dm' % minutes)
54 if seconds > 0 and not out:
55 # Skip seconds unless there's only seconds.
56 out.append('%02ds' % seconds)
57 return prefix + ''.join(out)
58
59
60class Stats(object):
61 def __init__(self):
62 self.total = 0
63 self.actually_reviewed = 0
[email protected]40b76872012-03-21 01:07:4464 self.latencies = []
[email protected]34dd5eb2011-09-23 21:11:1865 self.lgtms = 0
66 self.multiple_lgtms = 0
67 self.drive_by = 0
68 self.not_requested = 0
[email protected]2e36aad2011-09-25 00:50:4969 self.self_review = 0
[email protected]34dd5eb2011-09-23 21:11:1870
[email protected]34dd5eb2011-09-23 21:11:1871 self.percent_lgtm = 0.
72 self.percent_drive_by = 0.
73 self.percent_not_requested = 0.
[email protected]2e36aad2011-09-25 00:50:4974 self.days = 0
[email protected]34dd5eb2011-09-23 21:11:1875
[email protected]40b76872012-03-21 01:07:4476 @property
77 def average_latency(self):
78 if not self.latencies:
79 return 0
80 return sum(self.latencies) / float(len(self.latencies))
81
82 @property
83 def median_latency(self):
84 if not self.latencies:
85 return 0
86 length = len(self.latencies)
87 latencies = sorted(self.latencies)
88 if (length & 1) == 0:
[email protected]fbb00c42013-07-17 00:15:3989 return (latencies[length/2] + latencies[length/2-1]) / 2.
[email protected]40b76872012-03-21 01:07:4490 else:
91 return latencies[length/2]
92
93 @property
94 def percent_done(self):
95 if not self.total:
96 return 0
97 return self.actually_reviewed * 100. / self.total
98
99 @property
100 def review_per_day(self):
101 if not self.days:
102 return 0
103 return self.total * 1. / self.days
104
105 @property
106 def review_done_per_day(self):
107 if not self.days:
108 return 0
109 return self.actually_reviewed * 1. / self.days
[email protected]34dd5eb2011-09-23 21:11:18110
111 def finalize(self, first_day, last_day):
[email protected]34dd5eb2011-09-23 21:11:18112 if self.actually_reviewed:
[email protected]17f52822013-06-03 23:40:10113 assert self.actually_reviewed > 0
[email protected]34dd5eb2011-09-23 21:11:18114 self.percent_lgtm = (self.lgtms * 100. / self.actually_reviewed)
115 self.percent_drive_by = (self.drive_by * 100. / self.actually_reviewed)
116 self.percent_not_requested = (
117 self.not_requested * 100. / self.actually_reviewed)
[email protected]17f52822013-06-03 23:40:10118 assert bool(first_day) == bool(last_day)
[email protected]34dd5eb2011-09-23 21:11:18119 if first_day and last_day:
[email protected]dc33eae2014-12-11 17:15:26120 assert first_day <= last_day
[email protected]34dd5eb2011-09-23 21:11:18121 self.days = (to_datetime(last_day) - to_datetime(first_day)).days + 1
[email protected]17f52822013-06-03 23:40:10122 assert self.days > 0
[email protected]34dd5eb2011-09-23 21:11:18123
124
125def _process_issue_lgtms(issue, reviewer, stats):
126 """Calculates LGTMs stats."""
127 stats.actually_reviewed += 1
128 reviewer_lgtms = len([
129 msg for msg in issue['messages']
130 if msg['approval'] and msg['sender'] == reviewer])
131 if reviewer_lgtms > 1:
132 stats.multiple_lgtms += 1
133 return ' X '
134 if reviewer_lgtms:
135 stats.lgtms += 1
136 return ' x '
137 else:
138 return ' o '
139
140
141def _process_issue_latency(issue, reviewer, stats):
142 """Calculates latency for an issue that was actually reviewed."""
143 from_owner = [
144 msg for msg in issue['messages'] if msg['sender'] == issue['owner_email']
145 ]
146 if not from_owner:
147 # Probably requested by email.
148 stats.not_requested += 1
149 return '<no rqst sent>'
150
151 first_msg_from_owner = None
152 latency = None
153 received = False
154 for index, msg in enumerate(issue['messages']):
155 if not first_msg_from_owner and msg['sender'] == issue['owner_email']:
156 first_msg_from_owner = msg
157 if index and not received and msg['sender'] == reviewer:
158 # Not first email, reviewer never received one, reviewer sent a mesage.
159 stats.drive_by += 1
160 return '<drive-by>'
161 received |= reviewer in msg['recipients']
162
163 if first_msg_from_owner and msg['sender'] == reviewer:
164 delta = msg['date'] - first_msg_from_owner['date']
165 latency = delta.seconds + delta.days * 24 * 3600
166 break
167
168 if latency is None:
169 stats.not_requested += 1
170 return '<no rqst sent>'
171 if latency > 0:
[email protected]40b76872012-03-21 01:07:44172 stats.latencies.append(latency)
[email protected]34dd5eb2011-09-23 21:11:18173 else:
174 stats.not_requested += 1
175 return to_time(latency)
176
177
178def _process_issue(issue):
179 """Preprocesses the issue to simplify the remaining code."""
180 issue['owner_email'] = username(issue['owner_email'])
181 issue['reviewers'] = set(username(r) for r in issue['reviewers'])
182 # By default, hide commit-bot.
183 issue['reviewers'] -= set(['commit-bot'])
184 for msg in issue['messages']:
185 msg['sender'] = username(msg['sender'])
186 msg['recipients'] = [username(r) for r in msg['recipients']]
187 # Convert all times to datetime instances.
188 msg['date'] = to_datetime(msg['date'])
189 issue['messages'].sort(key=lambda x: x['date'])
190
191
192def print_issue(issue, reviewer, stats):
193 """Process an issue and prints stats about it."""
194 stats.total += 1
195 _process_issue(issue)
[email protected]2e36aad2011-09-25 00:50:49196 if issue['owner_email'] == reviewer:
197 stats.self_review += 1
198 latency = '<self review>'
199 reviewed = ''
200 elif any(msg['sender'] == reviewer for msg in issue['messages']):
[email protected]34dd5eb2011-09-23 21:11:18201 reviewed = _process_issue_lgtms(issue, reviewer, stats)
202 latency = _process_issue_latency(issue, reviewer, stats)
203 else:
204 latency = 'N/A'
205 reviewed = ''
206
207 # More information is available, print issue.keys() to see them.
208 print '%7d %10s %3s %14s %-15s %s' % (
209 issue['issue'],
210 issue['created'][:10],
211 reviewed,
212 latency,
213 issue['owner_email'],
214 ', '.join(sorted(issue['reviewers'])))
215
216
[email protected]0bccb922011-09-22 19:22:22217def print_reviews(reviewer, created_after, created_before, instance_url):
[email protected]34dd5eb2011-09-23 21:11:18218 """Prints issues |reviewer| received and potentially reviewed."""
[email protected]c73e5162011-09-21 23:16:12219 remote = rietveld.Rietveld(instance_url, None, None)
[email protected]34dd5eb2011-09-23 21:11:18220
221 # The stats we gather. Feel free to send me a CL to get more stats.
222 stats = Stats()
223
[email protected]34dd5eb2011-09-23 21:11:18224 # Column sizes need to match print_issue() output.
225 print >> sys.stderr, (
226 'Issue Creation Did Latency Owner Reviewers')
[email protected]c73e5162011-09-21 23:16:12227
228 # See def search() in rietveld.py to see all the filters you can use.
[email protected]17f52822013-06-03 23:40:10229 issues = []
[email protected]c73e5162011-09-21 23:16:12230 for issue in remote.search(
[email protected]c73e5162011-09-21 23:16:12231 reviewer=reviewer,
232 created_after=created_after,
233 created_before=created_before,
[email protected]34dd5eb2011-09-23 21:11:18234 with_messages=True):
[email protected]17f52822013-06-03 23:40:10235 issues.append(issue)
[email protected]34dd5eb2011-09-23 21:11:18236 print_issue(issue, username(reviewer), stats)
[email protected]17f52822013-06-03 23:40:10237
238 issues.sort(key=lambda x: x['created'])
239 first_day = None
240 last_day = None
241 if issues:
242 first_day = issues[0]['created'][:10]
243 last_day = issues[-1]['created'][:10]
[email protected]34dd5eb2011-09-23 21:11:18244 stats.finalize(first_day, last_day)
[email protected]c73e5162011-09-21 23:16:12245
[email protected]34dd5eb2011-09-23 21:11:18246 print >> sys.stderr, (
[email protected]2e36aad2011-09-25 00:50:49247 '%s reviewed %d issues out of %d (%1.1f%%). %d were self-review.' %
248 (reviewer, stats.actually_reviewed, stats.total, stats.percent_done,
249 stats.self_review))
[email protected]34dd5eb2011-09-23 21:11:18250 print >> sys.stderr, (
251 '%4.1f review request/day during %3d days (%4.1f r/d done).' % (
252 stats.review_per_day, stats.days, stats.review_done_per_day))
253 print >> sys.stderr, (
254 '%4d were drive-bys (%5.1f%% of reviews done).' % (
255 stats.drive_by, stats.percent_drive_by))
256 print >> sys.stderr, (
257 '%4d were requested over IM or irc (%5.1f%% of reviews done).' % (
258 stats.not_requested, stats.percent_not_requested))
259 print >> sys.stderr, (
260 ('%4d issues LGTM\'d (%5.1f%% of reviews done),'
261 ' gave multiple LGTMs on %d issues.') % (
262 stats.lgtms, stats.percent_lgtm, stats.multiple_lgtms))
263 print >> sys.stderr, (
264 'Average latency from request to first comment is %s.' %
265 to_time(stats.average_latency))
[email protected]40b76872012-03-21 01:07:44266 print >> sys.stderr, (
267 'Median latency from request to first comment is %s.' %
268 to_time(stats.median_latency))
[email protected]c73e5162011-09-21 23:16:12269
270
[email protected]0bccb922011-09-22 19:22:22271def print_count(reviewer, created_after, created_before, instance_url):
[email protected]b7704322011-09-22 15:22:05272 remote = rietveld.Rietveld(instance_url, None, None)
273 print len(list(remote.search(
[email protected]b7704322011-09-22 15:22:05274 reviewer=reviewer,
275 created_after=created_after,
276 created_before=created_before,
[email protected]0bccb922011-09-22 19:22:22277 keys_only=True)))
[email protected]b7704322011-09-22 15:22:05278
279
[email protected]c73e5162011-09-21 23:16:12280def get_previous_quarter(today):
281 """There are four quarters, 01-03, 04-06, 07-09, 10-12.
282
283 If today is in the last month of a quarter, assume it's the current quarter
284 that is requested.
285 """
[email protected]5e6868b2011-09-22 20:15:07286 end_year = today.year
287 end_month = today.month - (today.month % 3) + 1
288 if end_month <= 0:
289 end_year -= 1
290 end_month += 12
291 if end_month > 12:
292 end_year += 1
293 end_month -= 12
294 end = '%d-%02d-01' % (end_year, end_month)
295 begin_year = end_year
296 begin_month = end_month - 3
297 if begin_month <= 0:
298 begin_year -= 1
299 begin_month += 12
300 begin = '%d-%02d-01' % (begin_year, begin_month)
301 return begin, end
[email protected]c73e5162011-09-21 23:16:12302
303
304def main():
[email protected]b7704322011-09-22 15:22:05305 # Silence upload.py.
306 rietveld.upload.verbosity = 0
[email protected]70e91c02011-09-22 19:14:51307 today = datetime.date.today()
[email protected]5e6868b2011-09-22 20:15:07308 begin, end = get_previous_quarter(today)
[email protected]dc33eae2014-12-11 17:15:26309 default_email = os.environ.get('EMAIL_ADDRESS')
310 if not default_email:
311 user = os.environ.get('USER')
312 if user:
313 default_email = user + '@chromium.org'
314
315 parser = optparse.OptionParser(description=__doc__)
[email protected]b7704322011-09-22 15:22:05316 parser.add_option(
317 '--count', action='store_true',
318 help='Just count instead of printing individual issues')
[email protected]70e91c02011-09-22 19:14:51319 parser.add_option(
[email protected]dc33eae2014-12-11 17:15:26320 '-r', '--reviewer', metavar='<email>', default=default_email,
[email protected]0bccb922011-09-22 19:22:22321 help='Filter on issue reviewer, default=%default')
[email protected]70e91c02011-09-22 19:14:51322 parser.add_option(
[email protected]5e6868b2011-09-22 20:15:07323 '-b', '--begin', metavar='<date>',
[email protected]70e91c02011-09-22 19:14:51324 help='Filter issues created after the date')
325 parser.add_option(
[email protected]5e6868b2011-09-22 20:15:07326 '-e', '--end', metavar='<date>',
327 help='Filter issues created before the date')
[email protected]b7704322011-09-22 15:22:05328 parser.add_option(
329 '-Q', '--last_quarter', action='store_true',
[email protected]dc33eae2014-12-11 17:15:26330 help='Use last quarter\'s dates, e.g. %s to %s' % (begin, end))
[email protected]70e91c02011-09-22 19:14:51331 parser.add_option(
332 '-i', '--instance_url', metavar='<host>',
333 default='http://codereview.chromium.org',
334 help='Host to use, default is %default')
[email protected]c73e5162011-09-21 23:16:12335 # Remove description formatting
[email protected]14e37ad2011-11-30 20:26:16336 parser.format_description = (
337 lambda _: parser.description) # pylint: disable=E1101
[email protected]c73e5162011-09-21 23:16:12338 options, args = parser.parse_args()
339 if args:
340 parser.error('Args unsupported')
[email protected]dc33eae2014-12-11 17:15:26341 if options.reviewer is None:
342 parser.error('$EMAIL_ADDRESS and $USER are not set, please use -r')
343
[email protected]0bccb922011-09-22 19:22:22344 print >> sys.stderr, 'Searching for reviews by %s' % options.reviewer
[email protected]c73e5162011-09-21 23:16:12345 if options.last_quarter:
[email protected]5e6868b2011-09-22 20:15:07346 options.begin = begin
347 options.end = end
[email protected]b7704322011-09-22 15:22:05348 print >> sys.stderr, 'Using range %s to %s' % (
[email protected]5e6868b2011-09-22 20:15:07349 options.begin, options.end)
[email protected]dc33eae2014-12-11 17:15:26350 else:
351 if options.begin is None or options.end is None:
352 parser.error('Please specify either --last_quarter or --begin and --end')
[email protected]16c319d2014-03-11 20:04:20353
354 # Validate dates.
355 try:
356 to_datetime(options.begin)
357 to_datetime(options.end)
358 except ValueError as e:
359 parser.error('%s: %s - %s' % (e, options.begin, options.end))
360
[email protected]b7704322011-09-22 15:22:05361 if options.count:
362 print_count(
[email protected]0bccb922011-09-22 19:22:22363 options.reviewer,
[email protected]5e6868b2011-09-22 20:15:07364 options.begin,
365 options.end,
[email protected]b7704322011-09-22 15:22:05366 options.instance_url)
367 else:
368 print_reviews(
[email protected]0bccb922011-09-22 19:22:22369 options.reviewer,
[email protected]5e6868b2011-09-22 20:15:07370 options.begin,
371 options.end,
[email protected]b7704322011-09-22 15:22:05372 options.instance_url)
[email protected]c73e5162011-09-21 23:16:12373 return 0
374
375
376if __name__ == '__main__':
377 sys.exit(main())