| #!/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() |
| 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() |