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 consended string.""" | |
Dirk Pranke
2011/09/23 20:59:51
"consended"?
M-A Ruel
2011/09/23 21:05:14
compact.
| |
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 |