bug_bash.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. #!/usr/bin/env python3
  2. # Copyright (c) 2021, Facebook
  3. #
  4. # SPDX-License-Identifier: Apache-2.0
  5. """Query the Top-Ten Bug Bashers
  6. This script will query the top-ten Bug Bashers in a specified date window.
  7. Usage:
  8. ./scripts/bug-bash.py -t ~/.ghtoken -b 2021-07-26 -e 2021-08-07
  9. GITHUB_TOKEN="..." ./scripts/bug-bash.py -b 2021-07-26 -e 2021-08-07
  10. """
  11. import argparse
  12. from datetime import datetime, timedelta
  13. import operator
  14. import os
  15. # Requires PyGithub
  16. from github import Github
  17. def parse_args():
  18. parser = argparse.ArgumentParser()
  19. parser.add_argument('-a', '--all', dest='all',
  20. help='Show all bugs squashed', action='store_true')
  21. parser.add_argument('-t', '--token', dest='tokenfile',
  22. help='File containing GitHub token', metavar='FILE')
  23. parser.add_argument('-b', '--begin', dest='begin', help='begin date (YYYY-mm-dd)',
  24. metavar='date', type=valid_date_type, required=True)
  25. parser.add_argument('-e', '--end', dest='end', help='end date (YYYY-mm-dd)',
  26. metavar='date', type=valid_date_type, required=True)
  27. args = parser.parse_args()
  28. if args.end < args.begin:
  29. raise ValueError(
  30. 'end date {} is before begin date {}'.format(args.end, args.begin))
  31. if args.tokenfile:
  32. with open(args.tokenfile, 'r') as file:
  33. token = file.read()
  34. token = token.strip()
  35. else:
  36. if 'GITHUB_TOKEN' not in os.environ:
  37. raise ValueError('No credentials specified')
  38. token = os.environ['GITHUB_TOKEN']
  39. setattr(args, 'token', token)
  40. return args
  41. class BugBashTally(object):
  42. def __init__(self, gh, begin_date, end_date):
  43. """Create a BugBashTally object with the provided Github object,
  44. begin datetime object, and end datetime object"""
  45. self._gh = gh
  46. self._repo = gh.get_repo('zephyrproject-rtos/zephyr')
  47. self._begin_date = begin_date
  48. self._end_date = end_date
  49. self._issues = []
  50. self._pulls = []
  51. def get_tally(self):
  52. """Return a dict with (key = user, value = score)"""
  53. tally = dict()
  54. for p in self.get_pulls():
  55. user = p.user.login
  56. tally[user] = tally.get(user, 0) + 1
  57. return tally
  58. def get_rev_tally(self):
  59. """Return a dict with (key = score, value = list<user>) sorted in
  60. descending order"""
  61. # there may be ties!
  62. rev_tally = dict()
  63. for user, score in self.get_tally().items():
  64. if score not in rev_tally:
  65. rev_tally[score] = [user]
  66. else:
  67. rev_tally[score].append(user)
  68. # sort in descending order by score
  69. rev_tally = dict(
  70. sorted(rev_tally.items(), key=operator.itemgetter(0), reverse=True))
  71. return rev_tally
  72. def get_top_ten(self):
  73. """Return a dict with (key = score, value = user) sorted in
  74. descending order"""
  75. top_ten = []
  76. for score, users in self.get_rev_tally().items():
  77. # do not sort users by login - hopefully fair-ish
  78. for user in users:
  79. if len(top_ten) == 10:
  80. return top_ten
  81. top_ten.append(tuple([score, user]))
  82. return top_ten
  83. def get_pulls(self):
  84. """Return GitHub pull requests that squash bugs in the provided
  85. date window"""
  86. if self._pulls:
  87. return self._pulls
  88. self.get_issues()
  89. return self._pulls
  90. def get_issues(self):
  91. """Return GitHub issues representing bugs in the provided date
  92. window"""
  93. if self._issues:
  94. return self._issues
  95. cutoff = self._end_date + timedelta(1)
  96. issues = self._repo.get_issues(state='closed', labels=[
  97. 'bug'], since=self._begin_date)
  98. for i in issues:
  99. # the PyGithub API and v3 REST API do not facilitate 'until'
  100. # or 'end date' :-/
  101. if i.closed_at < self._begin_date or i.closed_at > cutoff:
  102. continue
  103. ipr = i.pull_request
  104. if ipr is None:
  105. # ignore issues without a linked pull request
  106. continue
  107. prid = int(ipr.html_url.split('/')[-1])
  108. pr = self._repo.get_pull(prid)
  109. if not pr.merged:
  110. # pull requests that were not merged do not count
  111. continue
  112. self._pulls.append(pr)
  113. self._issues.append(i)
  114. return self._issues
  115. # https://gist.github.com/monkut/e60eea811ef085a6540f
  116. def valid_date_type(arg_date_str):
  117. """custom argparse *date* type for user dates values given from the
  118. command line"""
  119. try:
  120. return datetime.strptime(arg_date_str, "%Y-%m-%d")
  121. except ValueError:
  122. msg = "Given Date ({0}) not valid! Expected format, YYYY-MM-DD!".format(arg_date_str)
  123. raise argparse.ArgumentTypeError(msg)
  124. def print_top_ten(top_ten):
  125. """Print the top-ten bug bashers"""
  126. for score, user in top_ten:
  127. # print tab-separated value, to allow for ./script ... > foo.csv
  128. print('{}\t{}'.format(score, user))
  129. def main():
  130. args = parse_args()
  131. bbt = BugBashTally(Github(args.token), args.begin, args.end)
  132. if args.all:
  133. # print one issue per line
  134. issues = bbt.get_issues()
  135. pulls = bbt.get_pulls()
  136. n = len(issues)
  137. m = len(pulls)
  138. assert n == m
  139. for i in range(0, n):
  140. print('{}\t{}\t{}'.format(
  141. issues[i].number, pulls[i].user.login, pulls[i].title))
  142. else:
  143. # print the top ten
  144. print_top_ten(bbt.get_top_ten())
  145. if __name__ == '__main__':
  146. main()