milestone_check.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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. try:
  10. api_token = os.environ['GITHUB_TOKEN']
  11. except KeyError:
  12. print('Error: set the environment variable GITHUB_TOKEN to a GitHub authentication token (see https://github.com/settings/tokens)')
  13. exit(1)
  14. MILESTONE='1.0'
  15. ranges = {
  16. 18: 'origin/0.35.0 --not origin/0.34.x', #0.35.0
  17. 20: 'origin/0.35.x --not v0.35.0', #0.35.x
  18. '1.0': 'origin/master --not origin/0.35.x',
  19. }
  20. out = subprocess.run("git log {} --format='%H,%cE,%s'".format(ranges[MILESTONE]), shell=True, encoding='utf8', stdout=subprocess.PIPE)
  21. commits = {i[0]: (i[1], i[2]) for i in (x.split(',',2) for x in out.stdout.splitlines())}
  22. url = 'https://api.github.com/graphql'
  23. json = { 'query' : """
  24. query test($cursor: String) {
  25. search(first: 50, after: $cursor, type: ISSUE, query: "repo:jupyterlab/jupyterlab milestone:%s is:pr is:merged ") {
  26. issueCount
  27. pageInfo {
  28. endCursor
  29. hasNextPage
  30. }
  31. nodes {
  32. ... on PullRequest {
  33. title
  34. number
  35. mergeCommit {
  36. oid
  37. }
  38. commits(first: 100) {
  39. totalCount
  40. nodes {
  41. commit {
  42. oid
  43. }
  44. }
  45. }
  46. }
  47. }
  48. }
  49. }
  50. """%MILESTONE,
  51. 'variables': {
  52. 'cursor': None
  53. }
  54. }
  55. headers = {'Authorization': 'token %s' % api_token}
  56. # construct a commit to PR dictionary
  57. prs = {}
  58. large_prs = []
  59. cursor = None
  60. while True:
  61. json['variables']['cursor'] = cursor
  62. r = requests.post(url=url, json=json, headers=headers)
  63. results = r.json()['data']['search']
  64. total_prs = results['issueCount']
  65. pr_list = results['nodes']
  66. for pr in pr_list:
  67. if pr['commits']['totalCount'] > 100:
  68. large_prs.append(pr['number'])
  69. print('Large PR, fetching commits individually: %s'%pr['number'])
  70. continue
  71. # TODO fetch commits
  72. prs[pr['number']] = {'mergeCommit': pr['mergeCommit']['oid'],
  73. 'commits': set(i['commit']['oid'] for i in pr['commits']['nodes'])}
  74. has_next_page = results['pageInfo']['hasNextPage']
  75. cursor = results['pageInfo']['endCursor']
  76. if not has_next_page:
  77. break
  78. prjson = {'query': """
  79. query test($pr:Int!, $cursor: String) {
  80. repository(owner: "jupyterlab", name: "jupyterlab") {
  81. pullRequest(number: $pr) {
  82. title
  83. number
  84. mergeCommit {
  85. oid
  86. }
  87. commits(first: 100, after: $cursor) {
  88. totalCount
  89. pageInfo {
  90. endCursor
  91. hasNextPage
  92. }
  93. nodes {
  94. commit {
  95. oid
  96. }
  97. }
  98. }
  99. }
  100. }
  101. }
  102. """, 'variables': {
  103. 'pr': None,
  104. 'cursor': None
  105. }}
  106. for prnumber in large_prs:
  107. prjson['variables']['pr']=prnumber
  108. pr_commits = set()
  109. while True:
  110. r = requests.post(url=url, json=prjson, headers=headers)
  111. pr = r.json()['data']['repository']['pullRequest']
  112. assert pr['number']==prnumber
  113. total_commits = pr['commits']['totalCount']
  114. pr_commits.update(i['commit']['oid'] for i in pr['commits']['nodes'])
  115. has_next_page = results['pageInfo']['hasNextPage']
  116. cursor = results['pageInfo']['endCursor']
  117. if not pr['commits']['pageInfo']['hasNextPage']:
  118. break
  119. prjson['variables']['cursor'] = pr['commits']['pageInfo']['endCursor']
  120. prs[prnumber] = {'mergeCommit': pr['mergeCommit']['oid'],
  121. 'commits': pr_commits}
  122. if total_commits > len(pr_commits):
  123. print("WARNING: PR %d has %d commits, but GitHub is only giving us %d of them"%(prnumber, total_commits, len(pr_commits)))
  124. # Check we got all PRs
  125. assert len(prs) == total_prs
  126. # Reverse dictionary
  127. commits_to_prs={}
  128. for key,value in prs.items():
  129. commits_to_prs[value['mergeCommit']]=key
  130. for c in value['commits']:
  131. commits_to_prs[c]=key
  132. # Check to see if commits in the repo are represented in PRs
  133. good = set()
  134. notfound = set()
  135. for c in commits:
  136. if c in commits_to_prs:
  137. good.add(commits_to_prs[c])
  138. else:
  139. notfound.add(c)
  140. prs_not_represented = set(prs.keys()) - good
  141. print("Milestone: %s, %d merged PRs"%(MILESTONE, total_prs))
  142. print("""
  143. PRs that are in the milestone, but have no commits in the version range.
  144. These PRs probably belong in a different milestone.
  145. """)
  146. print('\n'.join('https://github.com/jupyterlab/jupyterlab/pull/%d'%i for i in prs_not_represented))
  147. print('-'*40)
  148. print("""
  149. Commits that are not included in any PR on this milestone.
  150. This probably means the commit's PR needs to be assigned to this milestone,
  151. or the commit was pushed to master directly.
  152. """)
  153. print('\n'.join('%s %s %s'%(c, commits[c][0], commits[c][1]) for c in notfound))