Index: my_reviews.py |
diff --git a/my_reviews.py b/my_reviews.py |
index 3e1ab73251d7f008288e879362be2e0047680134..1d2377278ee3a9493b56cddcd97cfcd73749720d 100755 |
--- a/my_reviews.py |
+++ b/my_reviews.py |
@@ -9,6 +9,7 @@ Example: |
- my_reviews.py -r me@chromium.org -Q for stats for last quarter. |
""" |
import datetime |
+import math |
import optparse |
import os |
import sys |
@@ -17,48 +18,221 @@ import rietveld |
def username(email): |
+ """Keeps the username of an email address.""" |
return email.split('@', 1)[0] |
+def to_datetime(string): |
+ """Load UTC time as a string into a datetime object.""" |
+ try: |
+ # Format is 2011-07-05 01:26:12.084316 |
+ return datetime.datetime.strptime( |
+ string.split('.', 1)[0], '%Y-%m-%d %H:%M:%S') |
+ except ValueError: |
+ return datetime.datetime.strptime(string, '%Y-%m-%d') |
+ |
+ |
+def to_time(seconds): |
+ """Convert a number of seconds into human readable compact string.""" |
+ prefix = '' |
+ if seconds < 0: |
+ prefix = '-' |
+ seconds *= -1 |
+ minutes = math.floor(seconds / 60) |
+ seconds -= minutes * 60 |
+ hours = math.floor(minutes / 60) |
+ minutes -= hours * 60 |
+ days = math.floor(hours / 24) |
+ hours -= days * 24 |
+ out = [] |
+ if days > 0: |
+ out.append('%dd' % days) |
+ if hours > 0 or days > 0: |
+ out.append('%02dh' % hours) |
+ if minutes > 0 or hours > 0 or days > 0: |
+ out.append('%02dm' % minutes) |
+ if seconds > 0 and not out: |
+ # Skip seconds unless there's only seconds. |
+ out.append('%02ds' % seconds) |
+ return prefix + ''.join(out) |
+ |
+ |
+class Stats(object): |
+ def __init__(self): |
+ self.total = 0 |
+ self.actually_reviewed = 0 |
+ self.average_latency = 0. |
+ self.number_latency = 0 |
+ self.lgtms = 0 |
+ self.multiple_lgtms = 0 |
+ self.drive_by = 0 |
+ self.not_requested = 0 |
+ |
+ self.percent_done = 0. |
+ self.percent_lgtm = 0. |
+ self.percent_drive_by = 0. |
+ self.percent_not_requested = 0. |
+ self.days = None |
+ self.review_per_day = 0. |
+ self.review_done_per_day = 0. |
+ |
+ def add_latency(self, latency): |
+ self.average_latency = ( |
+ (self.average_latency * self.number_latency + latency) / |
+ (self.number_latency + 1.)) |
+ self.number_latency += 1 |
+ |
+ def finalize(self, first_day, last_day): |
+ if self.total: |
+ self.percent_done = (self.actually_reviewed * 100. / self.total) |
+ if self.actually_reviewed: |
+ self.percent_lgtm = (self.lgtms * 100. / self.actually_reviewed) |
+ self.percent_drive_by = (self.drive_by * 100. / self.actually_reviewed) |
+ self.percent_not_requested = ( |
+ self.not_requested * 100. / self.actually_reviewed) |
+ if first_day and last_day: |
+ self.days = (to_datetime(last_day) - to_datetime(first_day)).days + 1 |
+ if self.days: |
+ self.review_per_day = self.total * 1. / self.days |
+ self.review_done_per_day = self.actually_reviewed * 1. / self.days |
+ |
+ |
+def _process_issue_lgtms(issue, reviewer, stats): |
+ """Calculates LGTMs stats.""" |
+ stats.actually_reviewed += 1 |
+ reviewer_lgtms = len([ |
+ msg for msg in issue['messages'] |
+ if msg['approval'] and msg['sender'] == reviewer]) |
+ if reviewer_lgtms > 1: |
+ stats.multiple_lgtms += 1 |
+ return ' X ' |
+ if reviewer_lgtms: |
+ stats.lgtms += 1 |
+ return ' x ' |
+ else: |
+ return ' o ' |
+ |
+ |
+def _process_issue_latency(issue, reviewer, stats): |
+ """Calculates latency for an issue that was actually reviewed.""" |
+ from_owner = [ |
+ msg for msg in issue['messages'] if msg['sender'] == issue['owner_email'] |
+ ] |
+ if not from_owner: |
+ # Probably requested by email. |
+ stats.not_requested += 1 |
+ return '<no rqst sent>' |
+ |
+ first_msg_from_owner = None |
+ latency = None |
+ received = False |
+ for index, msg in enumerate(issue['messages']): |
+ if not first_msg_from_owner and msg['sender'] == issue['owner_email']: |
+ first_msg_from_owner = msg |
+ if index and not received and msg['sender'] == reviewer: |
+ # Not first email, reviewer never received one, reviewer sent a mesage. |
+ stats.drive_by += 1 |
+ return '<drive-by>' |
+ received |= reviewer in msg['recipients'] |
+ |
+ if first_msg_from_owner and msg['sender'] == reviewer: |
+ delta = msg['date'] - first_msg_from_owner['date'] |
+ latency = delta.seconds + delta.days * 24 * 3600 |
+ break |
+ |
+ if latency is None: |
+ stats.not_requested += 1 |
+ return '<no rqst sent>' |
+ if latency > 0: |
+ stats.add_latency(latency) |
+ else: |
+ stats.not_requested += 1 |
+ return to_time(latency) |
+ |
+ |
+def _process_issue(issue): |
+ """Preprocesses the issue to simplify the remaining code.""" |
+ issue['owner_email'] = username(issue['owner_email']) |
+ issue['reviewers'] = set(username(r) for r in issue['reviewers']) |
+ # By default, hide commit-bot. |
+ issue['reviewers'] -= set(['commit-bot']) |
+ for msg in issue['messages']: |
+ msg['sender'] = username(msg['sender']) |
+ msg['recipients'] = [username(r) for r in msg['recipients']] |
+ # Convert all times to datetime instances. |
+ msg['date'] = to_datetime(msg['date']) |
+ issue['messages'].sort(key=lambda x: x['date']) |
+ |
+ |
+def print_issue(issue, reviewer, stats): |
+ """Process an issue and prints stats about it.""" |
+ stats.total += 1 |
+ _process_issue(issue) |
+ if any(msg['sender'] == reviewer for msg in issue['messages']): |
+ reviewed = _process_issue_lgtms(issue, reviewer, stats) |
+ latency = _process_issue_latency(issue, reviewer, stats) |
+ else: |
+ latency = 'N/A' |
+ reviewed = '' |
+ |
+ # More information is available, print issue.keys() to see them. |
+ print '%7d %10s %3s %14s %-15s %s' % ( |
+ issue['issue'], |
+ issue['created'][:10], |
+ reviewed, |
+ latency, |
+ issue['owner_email'], |
+ ', '.join(sorted(issue['reviewers']))) |
+ |
+ |
def print_reviews(reviewer, created_after, created_before, instance_url): |
- """Prints issues the dude reviewed.""" |
+ """Prints issues |reviewer| received and potentially reviewed.""" |
remote = rietveld.Rietveld(instance_url, None, None) |
- total = 0 |
- actually_reviewed = 0 |
+ |
+ # The stats we gather. Feel free to send me a CL to get more stats. |
+ stats = Stats() |
+ |
+ last_issue = None |
+ first_day = None |
+ last_day = None |
+ |
+ # Column sizes need to match print_issue() output. |
+ print >> sys.stderr, ( |
+ 'Issue Creation Did Latency Owner Reviewers') |
# See def search() in rietveld.py to see all the filters you can use. |
for issue in remote.search( |
reviewer=reviewer, |
created_after=created_after, |
created_before=created_before, |
- with_messages=True, |
- ): |
- total += 1 |
- # By default, hide commit-bot and the domain. |
- reviewers = set(username(r) for r in issue['reviewers']) |
- reviewers -= set(['commit-bot']) |
- # Strip time. |
- timestamp = issue['created'][:10] |
- if any( |
- username(msg['sender']) == username(reviewer) |
- for msg in issue['messages']): |
- reviewed = ' x ' |
- actually_reviewed += 1 |
- else: |
- reviewed = ' ' |
- |
- # More information is available, print issue.keys() to see them. |
- print '%7d %s %s O:%-15s R:%s' % ( |
- issue['issue'], |
- timestamp, |
- reviewed, |
- username(issue['owner_email']), |
- ', '.join(reviewers)) |
- percent = 0. |
- if total: |
- percent = (actually_reviewed * 100. / total) |
- print 'You actually reviewed %d issues out of %d (%1.1f%%)' % ( |
- actually_reviewed, total, percent) |
+ with_messages=True): |
+ last_issue = issue |
+ if not first_day: |
+ first_day = issue['created'][:10] |
+ print_issue(issue, username(reviewer), stats) |
+ if last_issue: |
+ last_day = last_issue['created'][:10] |
+ stats.finalize(first_day, last_day) |
+ |
+ print >> sys.stderr, ( |
+ '%s reviewed %d issues out of %d (%1.1f%%).' % |
+ (reviewer, stats.actually_reviewed, stats.total, stats.percent_done)) |
+ print >> sys.stderr, ( |
+ '%4.1f review request/day during %3d days (%4.1f r/d done).' % ( |
+ stats.review_per_day, stats.days, stats.review_done_per_day)) |
+ print >> sys.stderr, ( |
+ '%4d were drive-bys (%5.1f%% of reviews done).' % ( |
+ stats.drive_by, stats.percent_drive_by)) |
+ print >> sys.stderr, ( |
+ '%4d were requested over IM or irc (%5.1f%% of reviews done).' % ( |
+ stats.not_requested, stats.percent_not_requested)) |
+ print >> sys.stderr, ( |
+ ('%4d issues LGTM\'d (%5.1f%% of reviews done),' |
+ ' gave multiple LGTMs on %d issues.') % ( |
+ stats.lgtms, stats.percent_lgtm, stats.multiple_lgtms)) |
+ print >> sys.stderr, ( |
+ 'Average latency from request to first comment is %s.' % |
+ to_time(stats.average_latency)) |
def print_count(reviewer, created_after, created_before, instance_url): |