ec7044437e
Disables allowing the python argparse library from automatically shortening command line arguments, this prevents issues whereby a new command is added and code that wrongly uses the shortened command of an existing argument which is the same as the new command being added will silently change script behaviour. Signed-off-by: Jamie McCrae <jamie.mccrae@nordicsemi.no>
188 lines
5.7 KiB
Python
Executable file
188 lines
5.7 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# Copyright (c) 2021, Facebook
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
"""Query the Top-Ten Bug Bashers
|
|
|
|
This script will query the top-ten Bug Bashers in a specified date window.
|
|
|
|
Usage:
|
|
./scripts/bug-bash.py -t ~/.ghtoken -b 2021-07-26 -e 2021-08-07
|
|
GITHUB_TOKEN="..." ./scripts/bug-bash.py -b 2021-07-26 -e 2021-08-07
|
|
"""
|
|
|
|
import argparse
|
|
from datetime import datetime, timedelta
|
|
import operator
|
|
import os
|
|
|
|
# Requires PyGithub
|
|
from github import Github
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(allow_abbrev=False)
|
|
parser.add_argument('-a', '--all', dest='all',
|
|
help='Show all bugs squashed', action='store_true')
|
|
parser.add_argument('-t', '--token', dest='tokenfile',
|
|
help='File containing GitHub token (alternatively, use GITHUB_TOKEN env variable)', metavar='FILE')
|
|
parser.add_argument('-s', '--start', dest='start', help='start date (YYYY-mm-dd)',
|
|
metavar='START_DATE', type=valid_date_type, required=True)
|
|
parser.add_argument('-e', '--end', dest='end', help='end date (YYYY-mm-dd)',
|
|
metavar='END_DATE', type=valid_date_type, required=True)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.end < args.start:
|
|
raise ValueError(
|
|
'end date {} is before start date {}'.format(args.end, args.start))
|
|
|
|
if args.tokenfile:
|
|
with open(args.tokenfile, 'r') as file:
|
|
token = file.read()
|
|
token = token.strip()
|
|
else:
|
|
if 'GITHUB_TOKEN' not in os.environ:
|
|
raise ValueError('No credentials specified')
|
|
token = os.environ['GITHUB_TOKEN']
|
|
|
|
setattr(args, 'token', token)
|
|
|
|
return args
|
|
|
|
|
|
class BugBashTally(object):
|
|
def __init__(self, gh, start_date, end_date):
|
|
"""Create a BugBashTally object with the provided Github object,
|
|
start datetime object, and end datetime object"""
|
|
self._gh = gh
|
|
self._repo = gh.get_repo('zephyrproject-rtos/zephyr')
|
|
self._start_date = start_date
|
|
self._end_date = end_date
|
|
|
|
self._issues = []
|
|
self._pulls = []
|
|
|
|
def get_tally(self):
|
|
"""Return a dict with (key = user, value = score)"""
|
|
tally = dict()
|
|
for p in self.get_pulls():
|
|
user = p.user.login
|
|
tally[user] = tally.get(user, 0) + 1
|
|
|
|
return tally
|
|
|
|
def get_rev_tally(self):
|
|
"""Return a dict with (key = score, value = list<user>) sorted in
|
|
descending order"""
|
|
# there may be ties!
|
|
rev_tally = dict()
|
|
for user, score in self.get_tally().items():
|
|
if score not in rev_tally:
|
|
rev_tally[score] = [user]
|
|
else:
|
|
rev_tally[score].append(user)
|
|
|
|
# sort in descending order by score
|
|
rev_tally = dict(
|
|
sorted(rev_tally.items(), key=operator.itemgetter(0), reverse=True))
|
|
|
|
return rev_tally
|
|
|
|
def get_top_ten(self):
|
|
"""Return a dict with (key = score, value = user) sorted in
|
|
descending order"""
|
|
top_ten = []
|
|
for score, users in self.get_rev_tally().items():
|
|
# do not sort users by login - hopefully fair-ish
|
|
for user in users:
|
|
if len(top_ten) == 10:
|
|
return top_ten
|
|
|
|
top_ten.append(tuple([score, user]))
|
|
|
|
return top_ten
|
|
|
|
def get_pulls(self):
|
|
"""Return GitHub pull requests that squash bugs in the provided
|
|
date window"""
|
|
if self._pulls:
|
|
return self._pulls
|
|
|
|
self.get_issues()
|
|
|
|
return self._pulls
|
|
|
|
def get_issues(self):
|
|
"""Return GitHub issues representing bugs in the provided date
|
|
window"""
|
|
if self._issues:
|
|
return self._issues
|
|
|
|
cutoff = self._end_date + timedelta(1)
|
|
issues = self._repo.get_issues(state='closed', labels=[
|
|
'bug'], since=self._start_date)
|
|
|
|
for i in issues:
|
|
# the PyGithub API and v3 REST API do not facilitate 'until'
|
|
# or 'end date' :-/
|
|
if i.closed_at < self._start_date or i.closed_at > cutoff:
|
|
continue
|
|
|
|
ipr = i.pull_request
|
|
if ipr is None:
|
|
# ignore issues without a linked pull request
|
|
continue
|
|
|
|
prid = int(ipr.html_url.split('/')[-1])
|
|
pr = self._repo.get_pull(prid)
|
|
if not pr.merged:
|
|
# pull requests that were not merged do not count
|
|
continue
|
|
|
|
self._pulls.append(pr)
|
|
self._issues.append(i)
|
|
|
|
return self._issues
|
|
|
|
|
|
# https://gist.github.com/monkut/e60eea811ef085a6540f
|
|
def valid_date_type(arg_date_str):
|
|
"""custom argparse *date* type for user dates values given from the
|
|
command line"""
|
|
try:
|
|
return datetime.strptime(arg_date_str, "%Y-%m-%d")
|
|
except ValueError:
|
|
msg = "Given Date ({0}) not valid! Expected format, YYYY-MM-DD!".format(arg_date_str)
|
|
raise argparse.ArgumentTypeError(msg)
|
|
|
|
|
|
def print_top_ten(top_ten):
|
|
"""Print the top-ten bug bashers"""
|
|
for score, user in top_ten:
|
|
# print tab-separated value, to allow for ./script ... > foo.csv
|
|
print('{}\t{}'.format(score, user))
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
bbt = BugBashTally(Github(args.token), args.start, args.end)
|
|
if args.all:
|
|
# print one issue per line
|
|
issues = bbt.get_issues()
|
|
pulls = bbt.get_pulls()
|
|
n = len(issues)
|
|
m = len(pulls)
|
|
assert n == m
|
|
for i in range(0, n):
|
|
print('{}\t{}\t{}'.format(
|
|
issues[i].number, pulls[i].user.login, pulls[i].title))
|
|
else:
|
|
# print the top ten
|
|
print_top_ten(bbt.get_top_ten())
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|