зеркало из https://github.com/mozilla/kitsune.git
354 строки
10 KiB
Python
Executable File
354 строки
10 KiB
Python
Executable File
#!/usr/bin/env python
|
|
import datetime
|
|
import subprocess
|
|
import sys
|
|
import textwrap
|
|
import urllib.request, urllib.parse, urllib.error
|
|
|
|
import requests
|
|
|
|
|
|
BUGZILLA_API_URL = 'https://bugzilla.mozilla.org/rest/'
|
|
BUGZILLA_PRODUCT = 'support.mozilla.org'
|
|
|
|
QUARTERS = {
|
|
1: [(1, 1), (3, 31)],
|
|
2: [(4, 1), (6, 30)],
|
|
3: [(7, 1), (9, 30)],
|
|
4: [(10, 1), (12, 31)]
|
|
}
|
|
|
|
USAGE = 'Usage: in_review.py <YEAR> [<QUARTER>]'
|
|
HEADER = 'in_review.py: find out what happened year or quarter!'
|
|
|
|
all_people = set()
|
|
|
|
|
|
def wrap(text, indent=' ', subsequent=' '):
|
|
text = text.split('\n\n')
|
|
text = [textwrap.fill(part, expand_tabs=True, initial_indent=indent,
|
|
subsequent_indent=subsequent)
|
|
for part in text]
|
|
return '\n\n'.join(text)
|
|
|
|
|
|
def fetch(url):
|
|
resp = requests.get(url)
|
|
if resp.status_code != 200:
|
|
raise Exception(resp.text)
|
|
|
|
json_data = resp.json()
|
|
|
|
if json_data.get('error'):
|
|
raise Exception('ERROR ' + repr(json_data))
|
|
|
|
return json_data
|
|
|
|
|
|
def fetch_bugs(params):
|
|
url = BUGZILLA_API_URL + '/bug' + '?' + urllib.parse.urlencode(params)
|
|
return fetch(url)
|
|
|
|
|
|
def fetch_bug_history(bugid):
|
|
url = BUGZILLA_API_URL + ('/bug/%d/history' % bugid)
|
|
return fetch(url)
|
|
|
|
|
|
def fetch_bug_comments(bugid):
|
|
url = BUGZILLA_API_URL + ('/bug/%d/comment' % bugid)
|
|
return fetch(url)
|
|
|
|
|
|
def print_bugzilla_stats(from_date, to_date):
|
|
# ------------------------------------------------
|
|
# Bug creation stats
|
|
# ------------------------------------------------
|
|
params = {
|
|
'product': BUGZILLA_PRODUCT,
|
|
'f1': 'creation_ts',
|
|
'o1': 'greaterthaneq',
|
|
'v1': from_date.strftime('%Y-%m-%d'),
|
|
'f2': 'creation_ts',
|
|
'o2': 'lessthan',
|
|
'v2': to_date.strftime('%Y-%m-%d')
|
|
}
|
|
|
|
json_data = fetch_bugs(params)
|
|
|
|
creation_count = len(json_data['bugs'])
|
|
|
|
creators = {}
|
|
for bug in json_data['bugs']:
|
|
creator = bug['creator_detail']['real_name']
|
|
if not creator:
|
|
creator = bug['creator'].split('@')[0]
|
|
creators[creator] = creators.get(creator, 0) + 1
|
|
all_people.add(creator)
|
|
|
|
print('Bugs created: %s' % creation_count)
|
|
print('Creators: %s' % len(creators))
|
|
print('')
|
|
creators = sorted(list(creators.items()), reverse=True, key=lambda item: item[1])
|
|
for person, count in creators:
|
|
print(' %34s : %s' % (person[:30].encode('utf-8'), count))
|
|
print('')
|
|
|
|
# ------------------------------------------------
|
|
# Bug resolution stats
|
|
# ------------------------------------------------
|
|
params = {
|
|
'product': BUGZILLA_PRODUCT,
|
|
'f1': 'cf_last_resolved',
|
|
'o1': 'greaterthaneq',
|
|
'v1': from_date.strftime('%Y-%m-%d'),
|
|
'f2': 'cf_last_resolved',
|
|
'o2': 'lessthan',
|
|
'v2': to_date.strftime('%Y-%m-%d')
|
|
}
|
|
|
|
json_data = fetch_bugs(params)
|
|
|
|
resolved_count = len(json_data['bugs'])
|
|
resolved_map = {}
|
|
resolvers = {}
|
|
traceback_bugs = []
|
|
research_bugs = []
|
|
tracker_bugs = []
|
|
commenters = {}
|
|
|
|
for bug in json_data['bugs']:
|
|
summary = bug['summary'].lower()
|
|
if summary.startswith('[traceback]'):
|
|
traceback_bugs.append(bug)
|
|
elif summary.startswith('[research]'):
|
|
research_bugs.append(bug)
|
|
elif summary.startswith('[tracker]'):
|
|
tracker_bugs.append(bug)
|
|
|
|
history = fetch_bug_history(bug['id'])
|
|
|
|
resolution = bug['resolution']
|
|
resolved_map[resolution] = resolved_map.get(resolution, 0) + 1
|
|
assigned = bug['assigned_to_detail']['real_name']
|
|
if not assigned:
|
|
assigned = bug['assigned_to'].split('@')[0]
|
|
|
|
if 'nobody' in assigned.lower():
|
|
# If no one was assigned, we give "credit" to whoever
|
|
# triaged the bug. We go through the history in reverse
|
|
# order because the "resolver" is the last person to
|
|
# resolve the bug.
|
|
for item in reversed(history['bugs'][0]['history']):
|
|
# See if this item in the history is the resolving event.
|
|
# If it is, then we know who resolved the bug and we
|
|
# can stop looking at history.
|
|
changes = [change for change in item['changes']
|
|
if change['field_name'] == 'status' and
|
|
change['added'] == 'RESOLVED']
|
|
|
|
if not changes:
|
|
continue
|
|
|
|
assigned = item['who']
|
|
break
|
|
|
|
if assigned:
|
|
if '@' in assigned:
|
|
assigned = assigned.split('@')[0]
|
|
|
|
resolvers[assigned] = resolvers.get(assigned, 0) + 1
|
|
all_people.add(assigned)
|
|
|
|
# Now get all the commenters
|
|
comments = fetch_bug_comments(bug['id'])
|
|
# The Bugzilla REST api has some interesting things about it.
|
|
for comment in comments['bugs'][str(bug['id'])]['comments']:
|
|
commenter = comment['author']
|
|
if '@' in commenter:
|
|
commenter = commenter.split('@')[0]
|
|
|
|
commenters[commenter] = commenters.get(commenter, 0) + 1
|
|
all_people.add(commenter)
|
|
|
|
print('Bugs resolved: %s' % resolved_count)
|
|
print('')
|
|
for resolution, count in list(resolved_map.items()):
|
|
print(' %34s : %s' % (resolution, count))
|
|
|
|
print('')
|
|
for title, count in [('Tracebacks', len(traceback_bugs)),
|
|
('Research', len(research_bugs)),
|
|
('Tracker', len(tracker_bugs))]:
|
|
print(' %34s : %s' % (title, count))
|
|
|
|
print('')
|
|
print('Research bugs: %s' % len(research_bugs))
|
|
print('')
|
|
for bug in research_bugs:
|
|
print(wrap('%s: %s' % (bug['id'], bug['summary']),
|
|
subsequent=' '))
|
|
|
|
print('')
|
|
print('Tracker bugs: %s' % len(tracker_bugs))
|
|
print('')
|
|
for bug in tracker_bugs:
|
|
print(wrap('%s: %s' % (bug['id'], bug['summary']),
|
|
subsequent=' '))
|
|
|
|
print('')
|
|
print('Resolvers: %s' % len(resolvers))
|
|
print('')
|
|
resolvers = sorted(list(resolvers.items()), reverse=True,
|
|
key=lambda item: item[1])
|
|
for person, count in resolvers:
|
|
print(' %34s : %s' % (person[:30].encode('utf-8'), count))
|
|
|
|
print('')
|
|
print('Commenters: %s' % len(commenters))
|
|
print('')
|
|
commenters = sorted(list(commenters.items()), reverse=True,
|
|
key=lambda item: item[1])
|
|
for person, count in commenters:
|
|
print(' %34s : %s' % (person[:30].encode('utf-8'), count))
|
|
|
|
|
|
def git(*args):
|
|
return subprocess.check_output(args)
|
|
|
|
|
|
def print_git_stats(from_date, to_date):
|
|
all_commits = subprocess.check_output([
|
|
'git', 'log',
|
|
'--after=' + from_date.strftime('%Y-%m-%d'),
|
|
'--before=' + to_date.strftime('%Y-%m-%d'),
|
|
'--format=%H'
|
|
])
|
|
|
|
all_commits = all_commits.splitlines()
|
|
|
|
# Person -> # commits
|
|
committers = {}
|
|
|
|
# Person -> (# files changed, # inserted, # deleted)
|
|
changes = {}
|
|
|
|
for commit in all_commits:
|
|
author = git('git', 'show', '-s', '--format=%an', commit)
|
|
author = author.strip().decode('utf-8')
|
|
|
|
committers[author] = committers.get(author, 0) + 1
|
|
all_people.add(author)
|
|
|
|
diff_data = git('git', 'show', '--numstat', '--format=oneline',
|
|
'--find-copies-harder', commit)
|
|
total_added = 0
|
|
total_deleted = 0
|
|
total_files = 0
|
|
|
|
# Skip the first line because it's the oneline summary and that's
|
|
# not part of the numstat data we want.
|
|
lines = diff_data.splitlines()[1:]
|
|
for line in lines:
|
|
if not line:
|
|
continue
|
|
added, deleted, fn = line.split('\t')
|
|
if fn.startswith('vendor/'):
|
|
continue
|
|
if added != '-':
|
|
total_added += int(added)
|
|
if deleted != '-':
|
|
total_deleted += int(deleted)
|
|
total_files += 1
|
|
|
|
old_changes = changes.get(author, (0, 0, 0))
|
|
changes[author] = (
|
|
old_changes[0] + total_added,
|
|
old_changes[1] + total_deleted,
|
|
old_changes[2] + total_files
|
|
)
|
|
|
|
print('Total commits:', len(all_commits))
|
|
print('')
|
|
|
|
committers = sorted(
|
|
list(committers.items()), key=lambda item: item[1], reverse=True)
|
|
for person, count in committers:
|
|
print(' %20s : %5s (+%s, -%s, files %s)' % (
|
|
person.encode('utf-8'), count,
|
|
changes[person][0], changes[person][1], changes[person][2]))
|
|
all_people.add(person)
|
|
|
|
# This is goofy summing, but whatevs.
|
|
print('')
|
|
print('Total lines added:', sum([item[0] for item in list(changes.values())]))
|
|
print('Total lines deleted:', sum([item[1] for item in list(changes.values())]))
|
|
print('Total files changed:', sum([item[2] for item in list(changes.values())]))
|
|
|
|
|
|
def print_all_people():
|
|
# We do this sorting thing to make it a little easier to suss out
|
|
# duplicates since we're pulling names from three different forms
|
|
# between Bugzilla and git. You're still going to have to go
|
|
# through it by hand to remove duplicates.
|
|
people = sorted(all_people, key=lambda a: a.lower())
|
|
|
|
for person in people:
|
|
print(' %s' % person.encode('utf-8'))
|
|
|
|
|
|
def print_header(text):
|
|
print('')
|
|
print(text)
|
|
print('=' * len(text))
|
|
print('')
|
|
|
|
|
|
def main(argv):
|
|
# XXX: This helps debug bugzilla xmlrpc bits.
|
|
# logging.basicConfig(level=logging.DEBUG)
|
|
|
|
if len(argv) < 1:
|
|
print(USAGE)
|
|
print('Error: Must specify year or year and quarter. Examples:')
|
|
print('in_review.py 2014')
|
|
print('in_review.py 2014 1')
|
|
return 1
|
|
|
|
print(HEADER)
|
|
|
|
year = int(argv[0])
|
|
if len(argv) == 1:
|
|
from_date = datetime.date(year, 1, 1)
|
|
to_date = datetime.date(year, 12, 31)
|
|
|
|
print_header('Year %s (%s -> %s)' % (year, from_date, to_date))
|
|
|
|
else:
|
|
quarter = int(argv[1])
|
|
quarter_dates = QUARTERS[quarter]
|
|
|
|
from_date = datetime.date(
|
|
year, quarter_dates[0][0], quarter_dates[0][1])
|
|
to_date = datetime.date(
|
|
year, quarter_dates[1][0], quarter_dates[1][1])
|
|
|
|
print_header('Quarter %sq%s (%s -> %s)' % (
|
|
year, quarter, from_date, to_date))
|
|
|
|
# Add 1 day because we do less-than.
|
|
to_date = to_date + datetime.timedelta(days=1)
|
|
|
|
print_header('Bugzilla')
|
|
print_bugzilla_stats(from_date, to_date)
|
|
|
|
print_header('git')
|
|
print_git_stats(from_date, to_date)
|
|
|
|
print_header('Everyone')
|
|
print_all_people()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main(sys.argv[1:]))
|