144 lines
5.0 KiB
Python
Executable File
144 lines
5.0 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# Copyright (C) 2011 - Soren Hansen
|
|
# Copyright (C) 2013 - Red Hat, Inc.
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import calendar
|
|
import datetime
|
|
import optparse
|
|
import os
|
|
import os.path
|
|
import sys
|
|
|
|
import utils
|
|
|
|
|
|
optparser = optparse.OptionParser()
|
|
optparser.add_option('-p', '--project', default='projects/nova.json',
|
|
help='JSON file describing the project to generate stats for')
|
|
optparser.add_option('-a', '--all', action='store_true',
|
|
help='Generate stats across all known projects (*.json)')
|
|
optparser.add_option('-u', '--user', default='russellb', help='gerrit user')
|
|
optparser.add_option('-k', '--key', default=None, help='ssh key for gerrit')
|
|
optparser.add_option('-s', '--stable', action='store_true',
|
|
help='Include stable branch commits')
|
|
optparser.add_option('-l', '--longest-waiting', type='int', default=5,
|
|
help='Show n changesets that have waited the longest)')
|
|
optparser.add_option('-m', '--waiting-more', type='int', default=7,
|
|
help='Show number of changesets that have waited more than n days)')
|
|
|
|
options, args = optparser.parse_args()
|
|
|
|
projects = utils.get_projects_info(options.project, options.all)
|
|
|
|
if not projects:
|
|
print "Please specify a project."
|
|
sys.exit(1)
|
|
|
|
changes = utils.get_changes(projects, options.user, options.key,
|
|
only_open=True)
|
|
|
|
waiting_on_submitter = []
|
|
waiting_on_reviewer = []
|
|
|
|
now = datetime.datetime.utcnow()
|
|
now_ts = calendar.timegm(now.timetuple())
|
|
|
|
def sec_to_period_string(seconds):
|
|
days = seconds / (3600 * 24)
|
|
hours = (seconds / 3600) - (days * 24)
|
|
minutes = (seconds / 60) - (days * 24 * 60) - (hours * 60)
|
|
return '%d days, %d hours, %d minutes' % (days, hours, minutes)
|
|
|
|
|
|
|
|
for change in changes:
|
|
if 'rowCount' in change:
|
|
continue
|
|
if not options.stable and 'stable' in change['branch']:
|
|
continue
|
|
if change['status'] != 'NEW':
|
|
# Filter out WORKINPROGRESS
|
|
continue
|
|
latest_patch = change['patchSets'][-1]
|
|
waiting_for_review = True
|
|
approvals = latest_patch.get('approvals', [])
|
|
approvals.sort(key=lambda a:a['grantedOn'])
|
|
for review in approvals:
|
|
if review['type'] not in ('CRVW', 'VRIF'):
|
|
continue
|
|
if review['value'] in ('-1', '-2'):
|
|
waiting_for_review = False
|
|
break
|
|
# The createdOn timestamp on the patch isn't what we want.
|
|
# It's when the patch was written, not submitted for review.
|
|
# The next best thing in the data we have is the time of the
|
|
# first review. When all is working well, jenkins or smokestack
|
|
# will comment within the first hour or two, so that's better
|
|
# than the other timestamp, which may reflect that the code
|
|
# was written many weeks ago, even though it was just recently
|
|
# submitted for review.
|
|
if approvals:
|
|
change['age'] = now_ts - approvals[0]['grantedOn']
|
|
else:
|
|
change['age'] = now_ts - latest_patch['createdOn']
|
|
if waiting_for_review:
|
|
waiting_on_reviewer.append(change)
|
|
else:
|
|
waiting_on_submitter.append(change)
|
|
|
|
|
|
def average_age(changes):
|
|
total_seconds = 0
|
|
for change in changes:
|
|
total_seconds += change['age']
|
|
avg_age = total_seconds / len(changes)
|
|
return sec_to_period_string(avg_age)
|
|
|
|
def median_age(changes):
|
|
changes = sorted(changes, key=lambda change: change['age'])
|
|
median_age = changes[len(changes)/2]['age']
|
|
return sec_to_period_string(median_age)
|
|
|
|
def number_waiting_more_than(changes, seconds):
|
|
index = 0
|
|
for change in changes:
|
|
if change['age'] > seconds:
|
|
return len(changes) - index
|
|
index += 1
|
|
return 0
|
|
|
|
age_sorted_waiting_on_reviewer = sorted(waiting_on_reviewer,
|
|
key=lambda change: change['age'])
|
|
|
|
|
|
print 'Projects: %s' % [project['name'] for project in projects]
|
|
print 'Total Open Reviews: %d' % (len(waiting_on_reviewer) +
|
|
len(waiting_on_submitter))
|
|
print 'Waiting on Submitter: %d' % len(waiting_on_submitter)
|
|
print 'Waiting on Reviewer: %d' % len(waiting_on_reviewer)
|
|
print ' --> Average wait time: %s' % average_age(waiting_on_reviewer)
|
|
print ' --> Median wait time: %s' % median_age(waiting_on_reviewer)
|
|
print ' --> Number waiting more than %i days: %i' % (
|
|
options.waiting_more, number_waiting_more_than(
|
|
age_sorted_waiting_on_reviewer,
|
|
60*60*24*options.waiting_more))
|
|
print ' --> Longest waiting reviews:'
|
|
for change in age_sorted_waiting_on_reviewer[-options.longest_waiting:]:
|
|
print ' --> %s %s \n (%s)' % (
|
|
sec_to_period_string(change['age']),
|
|
change['url'], change['subject'])
|