OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Get rietveld stats about the review you done, or forgot to do. | 6 """Get rietveld stats about the review you done, or forgot to do. |
7 | 7 |
8 Example: | 8 Example: |
9 - my_reviews.py -r me@chromium.org -Q for stats for last quarter. | 9 - my_reviews.py -r me@chromium.org -Q for stats for last quarter. |
10 """ | 10 """ |
11 import datetime | 11 import datetime |
| 12 import math |
12 import optparse | 13 import optparse |
13 import os | 14 import os |
14 import sys | 15 import sys |
15 | 16 |
16 import rietveld | 17 import rietveld |
17 | 18 |
18 | 19 |
19 def username(email): | 20 def username(email): |
| 21 """Keeps the username of an email address.""" |
20 return email.split('@', 1)[0] | 22 return email.split('@', 1)[0] |
21 | 23 |
22 | 24 |
| 25 def 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 |
| 35 def 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 |
| 60 class Stats(object): |
| 61 def __init__(self): |
| 62 self.total = 0 |
| 63 self.actually_reviewed = 0 |
| 64 self.average_latency = 0. |
| 65 self.number_latency = 0 |
| 66 self.lgtms = 0 |
| 67 self.multiple_lgtms = 0 |
| 68 self.drive_by = 0 |
| 69 self.not_requested = 0 |
| 70 |
| 71 self.percent_done = 0. |
| 72 self.percent_lgtm = 0. |
| 73 self.percent_drive_by = 0. |
| 74 self.percent_not_requested = 0. |
| 75 self.days = None |
| 76 self.review_per_day = 0. |
| 77 self.review_done_per_day = 0. |
| 78 |
| 79 def add_latency(self, latency): |
| 80 self.average_latency = ( |
| 81 (self.average_latency * self.number_latency + latency) / |
| 82 (self.number_latency + 1.)) |
| 83 self.number_latency += 1 |
| 84 |
| 85 def finalize(self, first_day, last_day): |
| 86 if self.total: |
| 87 self.percent_done = (self.actually_reviewed * 100. / self.total) |
| 88 if self.actually_reviewed: |
| 89 self.percent_lgtm = (self.lgtms * 100. / self.actually_reviewed) |
| 90 self.percent_drive_by = (self.drive_by * 100. / self.actually_reviewed) |
| 91 self.percent_not_requested = ( |
| 92 self.not_requested * 100. / self.actually_reviewed) |
| 93 if first_day and last_day: |
| 94 self.days = (to_datetime(last_day) - to_datetime(first_day)).days + 1 |
| 95 if self.days: |
| 96 self.review_per_day = self.total * 1. / self.days |
| 97 self.review_done_per_day = self.actually_reviewed * 1. / self.days |
| 98 |
| 99 |
| 100 def _process_issue_lgtms(issue, reviewer, stats): |
| 101 """Calculates LGTMs stats.""" |
| 102 stats.actually_reviewed += 1 |
| 103 reviewer_lgtms = len([ |
| 104 msg for msg in issue['messages'] |
| 105 if msg['approval'] and msg['sender'] == reviewer]) |
| 106 if reviewer_lgtms > 1: |
| 107 stats.multiple_lgtms += 1 |
| 108 return ' X ' |
| 109 if reviewer_lgtms: |
| 110 stats.lgtms += 1 |
| 111 return ' x ' |
| 112 else: |
| 113 return ' o ' |
| 114 |
| 115 |
| 116 def _process_issue_latency(issue, reviewer, stats): |
| 117 """Calculates latency for an issue that was actually reviewed.""" |
| 118 from_owner = [ |
| 119 msg for msg in issue['messages'] if msg['sender'] == issue['owner_email'] |
| 120 ] |
| 121 if not from_owner: |
| 122 # Probably requested by email. |
| 123 stats.not_requested += 1 |
| 124 return '<no rqst sent>' |
| 125 |
| 126 first_msg_from_owner = None |
| 127 latency = None |
| 128 received = False |
| 129 for index, msg in enumerate(issue['messages']): |
| 130 if not first_msg_from_owner and msg['sender'] == issue['owner_email']: |
| 131 first_msg_from_owner = msg |
| 132 if index and not received and msg['sender'] == reviewer: |
| 133 # Not first email, reviewer never received one, reviewer sent a mesage. |
| 134 stats.drive_by += 1 |
| 135 return '<drive-by>' |
| 136 received |= reviewer in msg['recipients'] |
| 137 |
| 138 if first_msg_from_owner and msg['sender'] == reviewer: |
| 139 delta = msg['date'] - first_msg_from_owner['date'] |
| 140 latency = delta.seconds + delta.days * 24 * 3600 |
| 141 break |
| 142 |
| 143 if latency is None: |
| 144 stats.not_requested += 1 |
| 145 return '<no rqst sent>' |
| 146 if latency > 0: |
| 147 stats.add_latency(latency) |
| 148 else: |
| 149 stats.not_requested += 1 |
| 150 return to_time(latency) |
| 151 |
| 152 |
| 153 def _process_issue(issue): |
| 154 """Preprocesses the issue to simplify the remaining code.""" |
| 155 issue['owner_email'] = username(issue['owner_email']) |
| 156 issue['reviewers'] = set(username(r) for r in issue['reviewers']) |
| 157 # By default, hide commit-bot. |
| 158 issue['reviewers'] -= set(['commit-bot']) |
| 159 for msg in issue['messages']: |
| 160 msg['sender'] = username(msg['sender']) |
| 161 msg['recipients'] = [username(r) for r in msg['recipients']] |
| 162 # Convert all times to datetime instances. |
| 163 msg['date'] = to_datetime(msg['date']) |
| 164 issue['messages'].sort(key=lambda x: x['date']) |
| 165 |
| 166 |
| 167 def print_issue(issue, reviewer, stats): |
| 168 """Process an issue and prints stats about it.""" |
| 169 stats.total += 1 |
| 170 _process_issue(issue) |
| 171 if any(msg['sender'] == reviewer for msg in issue['messages']): |
| 172 reviewed = _process_issue_lgtms(issue, reviewer, stats) |
| 173 latency = _process_issue_latency(issue, reviewer, stats) |
| 174 else: |
| 175 latency = 'N/A' |
| 176 reviewed = '' |
| 177 |
| 178 # More information is available, print issue.keys() to see them. |
| 179 print '%7d %10s %3s %14s %-15s %s' % ( |
| 180 issue['issue'], |
| 181 issue['created'][:10], |
| 182 reviewed, |
| 183 latency, |
| 184 issue['owner_email'], |
| 185 ', '.join(sorted(issue['reviewers']))) |
| 186 |
| 187 |
23 def print_reviews(reviewer, created_after, created_before, instance_url): | 188 def print_reviews(reviewer, created_after, created_before, instance_url): |
24 """Prints issues the dude reviewed.""" | 189 """Prints issues |reviewer| received and potentially reviewed.""" |
25 remote = rietveld.Rietveld(instance_url, None, None) | 190 remote = rietveld.Rietveld(instance_url, None, None) |
26 total = 0 | 191 |
27 actually_reviewed = 0 | 192 # The stats we gather. Feel free to send me a CL to get more stats. |
| 193 stats = Stats() |
| 194 |
| 195 last_issue = None |
| 196 first_day = None |
| 197 last_day = None |
| 198 |
| 199 # Column sizes need to match print_issue() output. |
| 200 print >> sys.stderr, ( |
| 201 'Issue Creation Did Latency Owner Reviewers') |
28 | 202 |
29 # See def search() in rietveld.py to see all the filters you can use. | 203 # See def search() in rietveld.py to see all the filters you can use. |
30 for issue in remote.search( | 204 for issue in remote.search( |
31 reviewer=reviewer, | 205 reviewer=reviewer, |
32 created_after=created_after, | 206 created_after=created_after, |
33 created_before=created_before, | 207 created_before=created_before, |
34 with_messages=True, | 208 with_messages=True): |
35 ): | 209 last_issue = issue |
36 total += 1 | 210 if not first_day: |
37 # By default, hide commit-bot and the domain. | 211 first_day = issue['created'][:10] |
38 reviewers = set(username(r) for r in issue['reviewers']) | 212 print_issue(issue, username(reviewer), stats) |
39 reviewers -= set(['commit-bot']) | 213 if last_issue: |
40 # Strip time. | 214 last_day = last_issue['created'][:10] |
41 timestamp = issue['created'][:10] | 215 stats.finalize(first_day, last_day) |
42 if any( | 216 |
43 username(msg['sender']) == username(reviewer) | 217 print >> sys.stderr, ( |
44 for msg in issue['messages']): | 218 '%s reviewed %d issues out of %d (%1.1f%%).' % |
45 reviewed = ' x ' | 219 (reviewer, stats.actually_reviewed, stats.total, stats.percent_done)) |
46 actually_reviewed += 1 | 220 print >> sys.stderr, ( |
47 else: | 221 '%4.1f review request/day during %3d days (%4.1f r/d done).' % ( |
48 reviewed = ' ' | 222 stats.review_per_day, stats.days, stats.review_done_per_day)) |
49 | 223 print >> sys.stderr, ( |
50 # More information is available, print issue.keys() to see them. | 224 '%4d were drive-bys (%5.1f%% of reviews done).' % ( |
51 print '%7d %s %s O:%-15s R:%s' % ( | 225 stats.drive_by, stats.percent_drive_by)) |
52 issue['issue'], | 226 print >> sys.stderr, ( |
53 timestamp, | 227 '%4d were requested over IM or irc (%5.1f%% of reviews done).' % ( |
54 reviewed, | 228 stats.not_requested, stats.percent_not_requested)) |
55 username(issue['owner_email']), | 229 print >> sys.stderr, ( |
56 ', '.join(reviewers)) | 230 ('%4d issues LGTM\'d (%5.1f%% of reviews done),' |
57 percent = 0. | 231 ' gave multiple LGTMs on %d issues.') % ( |
58 if total: | 232 stats.lgtms, stats.percent_lgtm, stats.multiple_lgtms)) |
59 percent = (actually_reviewed * 100. / total) | 233 print >> sys.stderr, ( |
60 print 'You actually reviewed %d issues out of %d (%1.1f%%)' % ( | 234 'Average latency from request to first comment is %s.' % |
61 actually_reviewed, total, percent) | 235 to_time(stats.average_latency)) |
62 | 236 |
63 | 237 |
64 def print_count(reviewer, created_after, created_before, instance_url): | 238 def print_count(reviewer, created_after, created_before, instance_url): |
65 remote = rietveld.Rietveld(instance_url, None, None) | 239 remote = rietveld.Rietveld(instance_url, None, None) |
66 print len(list(remote.search( | 240 print len(list(remote.search( |
67 reviewer=reviewer, | 241 reviewer=reviewer, |
68 created_after=created_after, | 242 created_after=created_after, |
69 created_before=created_before, | 243 created_before=created_before, |
70 keys_only=True))) | 244 keys_only=True))) |
71 | 245 |
(...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
144 print_reviews( | 318 print_reviews( |
145 options.reviewer, | 319 options.reviewer, |
146 options.begin, | 320 options.begin, |
147 options.end, | 321 options.end, |
148 options.instance_url) | 322 options.instance_url) |
149 return 0 | 323 return 0 |
150 | 324 |
151 | 325 |
152 if __name__ == '__main__': | 326 if __name__ == '__main__': |
153 sys.exit(main()) | 327 sys.exit(main()) |
OLD | NEW |