milestone_check.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. # Copyright (c) 2018 Jupyter Development Team.
  2. # Distributed under the terms of the Modified BSD License.
  3. # Generate a GitHub token at https://github.com/settings/tokens
  4. # Invoke this script using something like:
  5. # python scripts/milestone_check.py
  6. import subprocess
  7. import requests
  8. import os
  9. import sys
  10. ranges = {
  11. '0.35': 'origin/0.35.0 --not origin/0.34.x',
  12. '0.35.x': 'origin/0.35.x --not v0.35.0',
  13. '1.0': 'origin/1.0.x --not origin/0.35.x',
  14. '1.1': 'v1.1.0 --not origin/1.0.x',
  15. '1.1.1': 'v1.1.1 --not v1.1.0',
  16. '1.1.2': 'v1.1.2 --not v1.1.1',
  17. '1.1.3': 'v1.1.3 --not v1.1.2',
  18. '1.2': 'origin/1.x --not origin/1.1.x',
  19. '2.0': 'origin/master --not origin/1.x'
  20. }
  21. try:
  22. api_token = os.environ['GITHUB_TOKEN']
  23. except KeyError:
  24. print('Error: set the environment variable GITHUB_TOKEN to a GitHub authentication token (see https://github.com/settings/tokens)')
  25. exit(1)
  26. if len(sys.argv) != 2:
  27. print('Error: exactly one argument expected, the milestone.')
  28. exit(1)
  29. MILESTONE=sys.argv[1]
  30. if MILESTONE not in ranges:
  31. print('Error: I do not know about milestone %r. Possible milestones are %r'%(MILESTONE, list(ranges.keys())))
  32. exit(1)
  33. out = subprocess.run("git log {} --format='%H,%cE,%s'".format(ranges[MILESTONE]), shell=True, encoding='utf8', stdout=subprocess.PIPE)
  34. commits = {i[0]: (i[1], i[2]) for i in (x.split(',',2) for x in out.stdout.splitlines())}
  35. url = 'https://api.github.com/graphql'
  36. json = { 'query' : """
  37. query test($cursor: String) {
  38. search(first: 50, after: $cursor, type: ISSUE, query: "repo:jupyterlab/jupyterlab milestone:%s is:pr is:merged ") {
  39. issueCount
  40. pageInfo {
  41. endCursor
  42. hasNextPage
  43. }
  44. nodes {
  45. ... on PullRequest {
  46. title
  47. number
  48. mergeCommit {
  49. oid
  50. }
  51. commits(first: 100) {
  52. totalCount
  53. nodes {
  54. commit {
  55. oid
  56. }
  57. }
  58. }
  59. }
  60. }
  61. }
  62. }
  63. """%MILESTONE,
  64. 'variables': {
  65. 'cursor': None
  66. }
  67. }
  68. headers = {'Authorization': 'token %s' % api_token}
  69. # construct a commit to PR dictionary
  70. prs = {}
  71. large_prs = []
  72. cursor = None
  73. while True:
  74. json['variables']['cursor'] = cursor
  75. r = requests.post(url=url, json=json, headers=headers)
  76. results = r.json()['data']['search']
  77. total_prs = results['issueCount']
  78. pr_list = results['nodes']
  79. for pr in pr_list:
  80. if pr['commits']['totalCount'] > 100:
  81. large_prs.append(pr['number'])
  82. continue
  83. # TODO fetch commits
  84. prs[pr['number']] = {'mergeCommit': pr['mergeCommit']['oid'],
  85. 'commits': set(i['commit']['oid'] for i in pr['commits']['nodes'])}
  86. has_next_page = results['pageInfo']['hasNextPage']
  87. cursor = results['pageInfo']['endCursor']
  88. if not has_next_page:
  89. break
  90. prjson = {'query': """
  91. query test($pr:Int!, $cursor: String) {
  92. repository(owner: "jupyterlab", name: "jupyterlab") {
  93. pullRequest(number: $pr) {
  94. title
  95. number
  96. mergeCommit {
  97. oid
  98. }
  99. commits(first: 100, after: $cursor) {
  100. totalCount
  101. pageInfo {
  102. endCursor
  103. hasNextPage
  104. }
  105. nodes {
  106. commit {
  107. oid
  108. }
  109. }
  110. }
  111. }
  112. }
  113. }
  114. """, 'variables': {
  115. 'pr': None,
  116. 'cursor': None
  117. }}
  118. for prnumber in large_prs:
  119. prjson['variables']['pr']=prnumber
  120. pr_commits = set()
  121. while True:
  122. r = requests.post(url=url, json=prjson, headers=headers)
  123. pr = r.json()['data']['repository']['pullRequest']
  124. assert pr['number']==prnumber
  125. total_commits = pr['commits']['totalCount']
  126. pr_commits.update(i['commit']['oid'] for i in pr['commits']['nodes'])
  127. has_next_page = results['pageInfo']['hasNextPage']
  128. cursor = results['pageInfo']['endCursor']
  129. if not pr['commits']['pageInfo']['hasNextPage']:
  130. break
  131. prjson['variables']['cursor'] = pr['commits']['pageInfo']['endCursor']
  132. prs[prnumber] = {'mergeCommit': pr['mergeCommit']['oid'],
  133. 'commits': pr_commits}
  134. if total_commits > len(pr_commits):
  135. print("WARNING: PR %d (merge %s) has %d commits, but GitHub is only giving us %d of them"%(prnumber, pr['mergeCommit']['oid'], total_commits, len(pr_commits)))
  136. # Check we got all PRs
  137. assert len(prs) == total_prs
  138. # Reverse dictionary
  139. commits_to_prs={}
  140. for key,value in prs.items():
  141. commits_to_prs[value['mergeCommit']]=key
  142. for c in value['commits']:
  143. commits_to_prs[c]=key
  144. # Check to see if commits in the repo are represented in PRs
  145. good = set()
  146. notfound = set()
  147. for c in commits:
  148. if c in commits_to_prs:
  149. good.add(commits_to_prs[c])
  150. else:
  151. notfound.add(c)
  152. prs_not_represented = set(prs.keys()) - good
  153. print("Milestone: %s, %d merged PRs, %d commits in history"%(MILESTONE, total_prs, len(commits)))
  154. print()
  155. print('-'*40)
  156. print()
  157. if len(prs_not_represented) > 0:
  158. print("""
  159. PRs that are in the milestone, but have no commits in the version range.
  160. These PRs probably belong in a different milestone.
  161. """)
  162. print('\n'.join('https://github.com/jupyterlab/jupyterlab/pull/%d'%i for i in prs_not_represented))
  163. else:
  164. print('Congratulations! All PRs in this milestone have commits in the commit history for this version range, so they all probably belong in this milestone.')
  165. print()
  166. print('-'*40)
  167. print()
  168. if len(notfound):
  169. print("""The following commits are not included in any PR on this milestone.
  170. This probably means the commit's PR needs to be assigned to this milestone,
  171. or the commit was pushed to master directly.
  172. """)
  173. print('\n'.join('%s %s %s'%(c, commits[c][0], commits[c][1]) for c in notfound))
  174. prs_to_check = [c for c in notfound if 'Merge pull request #' in commits[c][1] and commits[c][0] == 'noreply@github.com']
  175. if len(prs_to_check)>0:
  176. print()
  177. print("Try checking these PRs. They probably should be in the milestone, but probably aren't:")
  178. print()
  179. print('\n'.join('%s %s'%(c, commits[c][1]) for c in prs_to_check))
  180. else:
  181. print('Congratulations! All commits in the commit history are included in some PR in this milestone.')