milestone_check.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  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 os
  7. import subprocess
  8. import sys
  9. import requests
  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": "v2.0.0 --not origin/1.x",
  20. "2.0.1": "v2.0.1 --not v2.0.0",
  21. "2.0.2": "origin/2.0.x --not v2.0.1",
  22. "2.1": "origin/2.1.x --not origin/2.0.x",
  23. "2.2": "origin/2.2.x --not origin/2.1.x",
  24. # 6507205805 is a commit in the debugger ancestor tree before merging
  25. "3.0": "origin/master ^origin/2.2.x ^6507205805",
  26. }
  27. try:
  28. api_token = os.environ["GITHUB_TOKEN"]
  29. except KeyError:
  30. print(
  31. "Error: set the environment variable GITHUB_TOKEN to a GitHub authentication token (see https://github.com/settings/tokens)"
  32. )
  33. exit(1)
  34. if len(sys.argv) != 2:
  35. print("Error: exactly one argument expected, the milestone.")
  36. exit(1)
  37. MILESTONE = sys.argv[1]
  38. if MILESTONE not in ranges:
  39. print(
  40. "Error: I do not know about milestone %r. Possible milestones are %r"
  41. % (MILESTONE, list(ranges.keys()))
  42. )
  43. exit(1)
  44. out = subprocess.run(
  45. "git log {} --format='%H,%cE,%s'".format(ranges[MILESTONE]),
  46. shell=True,
  47. encoding="utf8",
  48. stdout=subprocess.PIPE,
  49. )
  50. commits = {i[0]: (i[1], i[2]) for i in (x.split(",", 2) for x in out.stdout.splitlines())}
  51. url = "https://api.github.com/graphql"
  52. json = {
  53. "query": """
  54. query test($cursor: String) {
  55. search(first: 50, after: $cursor, type: ISSUE, query: "repo:jupyterlab/jupyterlab milestone:%s is:pr is:merged ") {
  56. issueCount
  57. pageInfo {
  58. endCursor
  59. hasNextPage
  60. }
  61. nodes {
  62. ... on PullRequest {
  63. title
  64. number
  65. mergeCommit {
  66. oid
  67. }
  68. commits(first: 100) {
  69. totalCount
  70. nodes {
  71. commit {
  72. oid
  73. }
  74. }
  75. }
  76. }
  77. }
  78. }
  79. }
  80. """
  81. % MILESTONE,
  82. "variables": {"cursor": None},
  83. }
  84. headers = {"Authorization": "token %s" % api_token}
  85. # construct a commit to PR dictionary
  86. prs = {}
  87. large_prs = []
  88. cursor = None
  89. while True:
  90. json["variables"]["cursor"] = cursor
  91. r = requests.post(url=url, json=json, headers=headers)
  92. results = r.json()["data"]["search"]
  93. total_prs = results["issueCount"]
  94. pr_list = results["nodes"]
  95. for pr in pr_list:
  96. if pr["commits"]["totalCount"] > 100:
  97. large_prs.append(pr["number"])
  98. continue
  99. # TODO fetch commits
  100. prs[pr["number"]] = {
  101. "mergeCommit": pr["mergeCommit"]["oid"],
  102. "commits": set(i["commit"]["oid"] for i in pr["commits"]["nodes"]),
  103. }
  104. has_next_page = results["pageInfo"]["hasNextPage"]
  105. cursor = results["pageInfo"]["endCursor"]
  106. if not has_next_page:
  107. break
  108. prjson = {
  109. "query": """
  110. query test($pr:Int!, $cursor: String) {
  111. repository(owner: "jupyterlab", name: "jupyterlab") {
  112. pullRequest(number: $pr) {
  113. title
  114. number
  115. mergeCommit {
  116. oid
  117. }
  118. commits(first: 100, after: $cursor) {
  119. totalCount
  120. pageInfo {
  121. endCursor
  122. hasNextPage
  123. }
  124. nodes {
  125. commit {
  126. oid
  127. }
  128. }
  129. }
  130. }
  131. }
  132. }
  133. """,
  134. "variables": {"pr": None, "cursor": None},
  135. }
  136. for prnumber in large_prs:
  137. prjson["variables"]["pr"] = prnumber
  138. pr_commits = set()
  139. while True:
  140. r = requests.post(url=url, json=prjson, headers=headers)
  141. pr = r.json()["data"]["repository"]["pullRequest"]
  142. assert pr["number"] == prnumber
  143. total_commits = pr["commits"]["totalCount"]
  144. pr_commits.update(i["commit"]["oid"] for i in pr["commits"]["nodes"])
  145. has_next_page = results["pageInfo"]["hasNextPage"]
  146. cursor = results["pageInfo"]["endCursor"]
  147. if not pr["commits"]["pageInfo"]["hasNextPage"]:
  148. break
  149. prjson["variables"]["cursor"] = pr["commits"]["pageInfo"]["endCursor"]
  150. prs[prnumber] = {"mergeCommit": pr["mergeCommit"]["oid"], "commits": pr_commits}
  151. if total_commits > len(pr_commits):
  152. print(
  153. "WARNING: PR %d (merge %s) has %d commits, but GitHub is only giving us %d of them"
  154. % (prnumber, pr["mergeCommit"]["oid"], total_commits, len(pr_commits))
  155. )
  156. # Check we got all PRs
  157. assert len(prs) == total_prs
  158. # Reverse dictionary
  159. commits_to_prs = {}
  160. for key, value in prs.items():
  161. commits_to_prs[value["mergeCommit"]] = key
  162. for c in value["commits"]:
  163. commits_to_prs[c] = key
  164. # Check to see if commits in the repo are represented in PRs
  165. good = set()
  166. notfound = set()
  167. for c in commits:
  168. if c in commits_to_prs:
  169. good.add(commits_to_prs[c])
  170. else:
  171. notfound.add(c)
  172. prs_not_represented = set(prs.keys()) - good
  173. print("Milestone: %s, %d merged PRs, %d commits in history" % (MILESTONE, total_prs, len(commits)))
  174. print()
  175. print("-" * 40)
  176. print()
  177. if len(prs_not_represented) > 0:
  178. print(
  179. """
  180. PRs that are in the milestone, but have no commits in the version range.
  181. These PRs probably belong in a different milestone.
  182. """
  183. )
  184. print(
  185. "\n".join(
  186. "https://github.com/jupyterlab/jupyterlab/pull/%d" % i for i in prs_not_represented
  187. )
  188. )
  189. else:
  190. print(
  191. "Congratulations! All PRs in this milestone have commits in the commit history for this version range, so they all probably belong in this milestone."
  192. )
  193. print()
  194. print("-" * 40)
  195. print()
  196. if len(notfound):
  197. print(
  198. """The following commits are not included in any PR on this milestone.
  199. This probably means the commit's PR needs to be assigned to this milestone,
  200. or the commit was pushed to master directly.
  201. """
  202. )
  203. print("\n".join("%s %s %s" % (c, commits[c][0], commits[c][1]) for c in notfound))
  204. prs_to_check = [
  205. c
  206. for c in notfound
  207. if "Merge pull request #" in commits[c][1] and commits[c][0] == "noreply@github.com"
  208. ]
  209. if len(prs_to_check) > 0:
  210. print()
  211. print(
  212. "Try checking these PRs. They probably should be in the milestone, but probably aren't:"
  213. )
  214. print()
  215. print("\n".join("%s %s" % (c, commits[c][1]) for c in prs_to_check))
  216. else:
  217. print(
  218. "Congratulations! All commits in the commit history are included in some PR in this milestone."
  219. )