pr-merge-queue 0.0.0-stage → 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +125 -2
  3. package/dist/claim-pr/claim-pr.d.mts +45 -0
  4. package/dist/claim-pr/claim-pr.d.mts.map +1 -0
  5. package/dist/claim-pr/claim-pr.mjs +217 -0
  6. package/dist/claim-pr/claim-pr.mjs.map +1 -0
  7. package/dist/claim-pr/index.d.mts +2 -0
  8. package/dist/claim-pr/index.d.mts.map +1 -0
  9. package/dist/claim-pr/index.mjs +2 -0
  10. package/dist/claim-pr/index.mjs.map +1 -0
  11. package/dist/cmd/claim-pr.d.mts +3 -0
  12. package/dist/cmd/claim-pr.d.mts.map +1 -0
  13. package/dist/cmd/claim-pr.mjs +20 -0
  14. package/dist/cmd/claim-pr.mjs.map +1 -0
  15. package/dist/cmd/open-pr.d.mts +3 -0
  16. package/dist/cmd/open-pr.d.mts.map +1 -0
  17. package/dist/cmd/open-pr.mjs +20 -0
  18. package/dist/cmd/open-pr.mjs.map +1 -0
  19. package/dist/cmd/pr-report.d.mts +3 -0
  20. package/dist/cmd/pr-report.d.mts.map +1 -0
  21. package/dist/cmd/pr-report.mjs +20 -0
  22. package/dist/cmd/pr-report.mjs.map +1 -0
  23. package/dist/cmd/push-fixed-pr.d.mts +3 -0
  24. package/dist/cmd/push-fixed-pr.d.mts.map +1 -0
  25. package/dist/cmd/push-fixed-pr.mjs +20 -0
  26. package/dist/cmd/push-fixed-pr.mjs.map +1 -0
  27. package/dist/cmd/report-unowned-prs.d.mts +3 -0
  28. package/dist/cmd/report-unowned-prs.d.mts.map +1 -0
  29. package/dist/cmd/report-unowned-prs.mjs +20 -0
  30. package/dist/cmd/report-unowned-prs.mjs.map +1 -0
  31. package/dist/cmd/unblock-prs.d.mts +3 -0
  32. package/dist/cmd/unblock-prs.d.mts.map +1 -0
  33. package/dist/cmd/unblock-prs.mjs +19 -0
  34. package/dist/cmd/unblock-prs.mjs.map +1 -0
  35. package/dist/entry-point.d.mts +3 -0
  36. package/dist/entry-point.d.mts.map +1 -0
  37. package/dist/entry-point.mjs +3 -0
  38. package/dist/entry-point.mjs.map +1 -0
  39. package/dist/lib/cli-args.d.mts +27 -0
  40. package/dist/lib/cli-args.d.mts.map +1 -0
  41. package/dist/lib/cli-args.mjs +55 -0
  42. package/dist/lib/cli-args.mjs.map +1 -0
  43. package/dist/lib/github-api.d.mts +153 -0
  44. package/dist/lib/github-api.d.mts.map +1 -0
  45. package/dist/lib/github-api.mjs +291 -0
  46. package/dist/lib/github-api.mjs.map +1 -0
  47. package/dist/lib/index.d.mts +7 -0
  48. package/dist/lib/index.d.mts.map +1 -0
  49. package/dist/lib/index.mjs +7 -0
  50. package/dist/lib/index.mjs.map +1 -0
  51. package/dist/lib/json.d.mts +12 -0
  52. package/dist/lib/json.d.mts.map +1 -0
  53. package/dist/lib/json.mjs +28 -0
  54. package/dist/lib/json.mjs.map +1 -0
  55. package/dist/lib/log.d.mts +10 -0
  56. package/dist/lib/log.d.mts.map +1 -0
  57. package/dist/lib/log.mjs +13 -0
  58. package/dist/lib/log.mjs.map +1 -0
  59. package/dist/lib/shell.d.mts +18 -0
  60. package/dist/lib/shell.d.mts.map +1 -0
  61. package/dist/lib/shell.mjs +42 -0
  62. package/dist/lib/shell.mjs.map +1 -0
  63. package/dist/lib/time.d.mts +17 -0
  64. package/dist/lib/time.d.mts.map +1 -0
  65. package/dist/lib/time.mjs +35 -0
  66. package/dist/lib/time.mjs.map +1 -0
  67. package/dist/open-pr/github.d.mts +72 -0
  68. package/dist/open-pr/github.d.mts.map +1 -0
  69. package/dist/open-pr/github.mjs +249 -0
  70. package/dist/open-pr/github.mjs.map +1 -0
  71. package/dist/open-pr/index.d.mts +6 -0
  72. package/dist/open-pr/index.d.mts.map +1 -0
  73. package/dist/open-pr/index.mjs +6 -0
  74. package/dist/open-pr/index.mjs.map +1 -0
  75. package/dist/open-pr/main.d.mts +36 -0
  76. package/dist/open-pr/main.d.mts.map +1 -0
  77. package/dist/open-pr/main.mjs +204 -0
  78. package/dist/open-pr/main.mjs.map +1 -0
  79. package/dist/open-pr/options.d.mts +20 -0
  80. package/dist/open-pr/options.d.mts.map +1 -0
  81. package/dist/open-pr/options.mjs +66 -0
  82. package/dist/open-pr/options.mjs.map +1 -0
  83. package/dist/open-pr/steps.d.mts +23 -0
  84. package/dist/open-pr/steps.d.mts.map +1 -0
  85. package/dist/open-pr/steps.mjs +38 -0
  86. package/dist/open-pr/steps.mjs.map +1 -0
  87. package/dist/open-pr/types.d.mts +179 -0
  88. package/dist/open-pr/types.d.mts.map +1 -0
  89. package/dist/open-pr/types.mjs +20 -0
  90. package/dist/open-pr/types.mjs.map +1 -0
  91. package/dist/pr-report/github.d.mts +59 -0
  92. package/dist/pr-report/github.d.mts.map +1 -0
  93. package/dist/pr-report/github.mjs +561 -0
  94. package/dist/pr-report/github.mjs.map +1 -0
  95. package/dist/pr-report/index.d.mts +8 -0
  96. package/dist/pr-report/index.d.mts.map +1 -0
  97. package/dist/pr-report/index.mjs +8 -0
  98. package/dist/pr-report/index.mjs.map +1 -0
  99. package/dist/pr-report/linked-issues.d.mts +15 -0
  100. package/dist/pr-report/linked-issues.d.mts.map +1 -0
  101. package/dist/pr-report/linked-issues.mjs +39 -0
  102. package/dist/pr-report/linked-issues.mjs.map +1 -0
  103. package/dist/pr-report/main.d.mts +42 -0
  104. package/dist/pr-report/main.d.mts.map +1 -0
  105. package/dist/pr-report/main.mjs +163 -0
  106. package/dist/pr-report/main.mjs.map +1 -0
  107. package/dist/pr-report/options.d.mts +36 -0
  108. package/dist/pr-report/options.d.mts.map +1 -0
  109. package/dist/pr-report/options.mjs +90 -0
  110. package/dist/pr-report/options.mjs.map +1 -0
  111. package/dist/pr-report/queue.d.mts +28 -0
  112. package/dist/pr-report/queue.d.mts.map +1 -0
  113. package/dist/pr-report/queue.mjs +74 -0
  114. package/dist/pr-report/queue.mjs.map +1 -0
  115. package/dist/pr-report/render.d.mts +14 -0
  116. package/dist/pr-report/render.d.mts.map +1 -0
  117. package/dist/pr-report/render.mjs +318 -0
  118. package/dist/pr-report/render.mjs.map +1 -0
  119. package/dist/pr-report/repo-settings.d.mts +12 -0
  120. package/dist/pr-report/repo-settings.d.mts.map +1 -0
  121. package/dist/pr-report/repo-settings.mjs +22 -0
  122. package/dist/pr-report/repo-settings.mjs.map +1 -0
  123. package/dist/push-fixed-pr/index.d.mts +2 -0
  124. package/dist/push-fixed-pr/index.d.mts.map +1 -0
  125. package/dist/push-fixed-pr/index.mjs +2 -0
  126. package/dist/push-fixed-pr/index.mjs.map +1 -0
  127. package/dist/push-fixed-pr/push-fixed-pr.d.mts +54 -0
  128. package/dist/push-fixed-pr/push-fixed-pr.d.mts.map +1 -0
  129. package/dist/push-fixed-pr/push-fixed-pr.mjs +172 -0
  130. package/dist/push-fixed-pr/push-fixed-pr.mjs.map +1 -0
  131. package/dist/report-unowned-prs/index.d.mts +2 -0
  132. package/dist/report-unowned-prs/index.d.mts.map +1 -0
  133. package/dist/report-unowned-prs/index.mjs +2 -0
  134. package/dist/report-unowned-prs/index.mjs.map +1 -0
  135. package/dist/report-unowned-prs/report-unowned-prs.d.mts +49 -0
  136. package/dist/report-unowned-prs/report-unowned-prs.d.mts.map +1 -0
  137. package/dist/report-unowned-prs/report-unowned-prs.mjs +127 -0
  138. package/dist/report-unowned-prs/report-unowned-prs.mjs.map +1 -0
  139. package/dist/tsconfig.json +1 -0
  140. package/dist/unblock-prs/auto-fix.d.mts +45 -0
  141. package/dist/unblock-prs/auto-fix.d.mts.map +1 -0
  142. package/dist/unblock-prs/auto-fix.mjs +207 -0
  143. package/dist/unblock-prs/auto-fix.mjs.map +1 -0
  144. package/dist/unblock-prs/checks.d.mts +307 -0
  145. package/dist/unblock-prs/checks.d.mts.map +1 -0
  146. package/dist/unblock-prs/checks.mjs +372 -0
  147. package/dist/unblock-prs/checks.mjs.map +1 -0
  148. package/dist/unblock-prs/claims.d.mts +21 -0
  149. package/dist/unblock-prs/claims.d.mts.map +1 -0
  150. package/dist/unblock-prs/claims.mjs +23 -0
  151. package/dist/unblock-prs/claims.mjs.map +1 -0
  152. package/dist/unblock-prs/config.d.mts +487 -0
  153. package/dist/unblock-prs/config.d.mts.map +1 -0
  154. package/dist/unblock-prs/config.mjs +119 -0
  155. package/dist/unblock-prs/config.mjs.map +1 -0
  156. package/dist/unblock-prs/constants.d.mts +28 -0
  157. package/dist/unblock-prs/constants.d.mts.map +1 -0
  158. package/dist/unblock-prs/constants.mjs +28 -0
  159. package/dist/unblock-prs/constants.mjs.map +1 -0
  160. package/dist/unblock-prs/demotions.d.mts +42 -0
  161. package/dist/unblock-prs/demotions.d.mts.map +1 -0
  162. package/dist/unblock-prs/demotions.mjs +50 -0
  163. package/dist/unblock-prs/demotions.mjs.map +1 -0
  164. package/dist/unblock-prs/github.d.mts +739 -0
  165. package/dist/unblock-prs/github.d.mts.map +1 -0
  166. package/dist/unblock-prs/github.mjs +626 -0
  167. package/dist/unblock-prs/github.mjs.map +1 -0
  168. package/dist/unblock-prs/index.d.mts +25 -0
  169. package/dist/unblock-prs/index.d.mts.map +1 -0
  170. package/dist/unblock-prs/index.mjs +25 -0
  171. package/dist/unblock-prs/index.mjs.map +1 -0
  172. package/dist/unblock-prs/labels.d.mts +49 -0
  173. package/dist/unblock-prs/labels.d.mts.map +1 -0
  174. package/dist/unblock-prs/labels.mjs +49 -0
  175. package/dist/unblock-prs/labels.mjs.map +1 -0
  176. package/dist/unblock-prs/main.d.mts +179 -0
  177. package/dist/unblock-prs/main.d.mts.map +1 -0
  178. package/dist/unblock-prs/main.mjs +640 -0
  179. package/dist/unblock-prs/main.mjs.map +1 -0
  180. package/dist/unblock-prs/merge-after.d.mts +46 -0
  181. package/dist/unblock-prs/merge-after.d.mts.map +1 -0
  182. package/dist/unblock-prs/merge-after.mjs +65 -0
  183. package/dist/unblock-prs/merge-after.mjs.map +1 -0
  184. package/dist/unblock-prs/options.d.mts +35 -0
  185. package/dist/unblock-prs/options.d.mts.map +1 -0
  186. package/dist/unblock-prs/options.mjs +88 -0
  187. package/dist/unblock-prs/options.mjs.map +1 -0
  188. package/dist/unblock-prs/owner.d.mts +32 -0
  189. package/dist/unblock-prs/owner.d.mts.map +1 -0
  190. package/dist/unblock-prs/owner.mjs +63 -0
  191. package/dist/unblock-prs/owner.mjs.map +1 -0
  192. package/dist/unblock-prs/quiet.d.mts +21 -0
  193. package/dist/unblock-prs/quiet.d.mts.map +1 -0
  194. package/dist/unblock-prs/quiet.mjs +42 -0
  195. package/dist/unblock-prs/quiet.mjs.map +1 -0
  196. package/dist/unblock-prs/rebase.d.mts +120 -0
  197. package/dist/unblock-prs/rebase.d.mts.map +1 -0
  198. package/dist/unblock-prs/rebase.mjs +576 -0
  199. package/dist/unblock-prs/rebase.mjs.map +1 -0
  200. package/dist/unblock-prs/release.d.mts +62 -0
  201. package/dist/unblock-prs/release.d.mts.map +1 -0
  202. package/dist/unblock-prs/release.mjs +94 -0
  203. package/dist/unblock-prs/release.mjs.map +1 -0
  204. package/dist/unblock-prs/set-aside-comment.d.mts +20 -0
  205. package/dist/unblock-prs/set-aside-comment.d.mts.map +1 -0
  206. package/dist/unblock-prs/set-aside-comment.mjs +93 -0
  207. package/dist/unblock-prs/set-aside-comment.mjs.map +1 -0
  208. package/dist/unblock-prs/set-aside-detail.d.mts +41 -0
  209. package/dist/unblock-prs/set-aside-detail.d.mts.map +1 -0
  210. package/dist/unblock-prs/set-aside-detail.mjs +56 -0
  211. package/dist/unblock-prs/set-aside-detail.mjs.map +1 -0
  212. package/dist/unblock-prs/skips.d.mts +85 -0
  213. package/dist/unblock-prs/skips.d.mts.map +1 -0
  214. package/dist/unblock-prs/skips.mjs +141 -0
  215. package/dist/unblock-prs/skips.mjs.map +1 -0
  216. package/dist/unblock-prs/stack.d.mts +98 -0
  217. package/dist/unblock-prs/stack.d.mts.map +1 -0
  218. package/dist/unblock-prs/stack.mjs +141 -0
  219. package/dist/unblock-prs/stack.mjs.map +1 -0
  220. package/dist/unblock-prs/triage.d.mts +28 -0
  221. package/dist/unblock-prs/triage.d.mts.map +1 -0
  222. package/dist/unblock-prs/triage.mjs +396 -0
  223. package/dist/unblock-prs/triage.mjs.map +1 -0
  224. package/dist/unblock-prs/types.d.mts +793 -0
  225. package/dist/unblock-prs/types.d.mts.map +1 -0
  226. package/dist/unblock-prs/types.mjs +26 -0
  227. package/dist/unblock-prs/types.mjs.map +1 -0
  228. package/dist/unblock-prs/util.d.mts +23 -0
  229. package/dist/unblock-prs/util.d.mts.map +1 -0
  230. package/dist/unblock-prs/util.mjs +60 -0
  231. package/dist/unblock-prs/util.mjs.map +1 -0
  232. package/dist/unblock-prs/version-pr.d.mts +57 -0
  233. package/dist/unblock-prs/version-pr.d.mts.map +1 -0
  234. package/dist/unblock-prs/version-pr.mjs +79 -0
  235. package/dist/unblock-prs/version-pr.mjs.map +1 -0
  236. package/dist/unblock-prs/watch.d.mts +17 -0
  237. package/dist/unblock-prs/watch.d.mts.map +1 -0
  238. package/dist/unblock-prs/watch.mjs +128 -0
  239. package/dist/unblock-prs/watch.mjs.map +1 -0
  240. package/package.json +118 -3
  241. package/src/claim-pr/claim-pr.mts +380 -0
  242. package/src/claim-pr/claim-pr.test.mts +176 -0
  243. package/src/claim-pr/index.mts +1 -0
  244. package/src/cmd/claim-pr.mts +32 -0
  245. package/src/cmd/open-pr.mts +28 -0
  246. package/src/cmd/pr-report.mts +28 -0
  247. package/src/cmd/push-fixed-pr.mts +36 -0
  248. package/src/cmd/report-unowned-prs.mts +37 -0
  249. package/src/cmd/unblock-prs.mts +24 -0
  250. package/src/entry-point.mts +3 -0
  251. package/src/lib/cli-args.mts +86 -0
  252. package/src/lib/cli-args.test.mts +92 -0
  253. package/src/lib/github-api.mts +463 -0
  254. package/src/lib/github-api.test.mts +40 -0
  255. package/src/lib/index.mts +6 -0
  256. package/src/lib/json.mts +43 -0
  257. package/src/lib/json.test.mts +34 -0
  258. package/src/lib/log.mts +19 -0
  259. package/src/lib/log.test.mts +45 -0
  260. package/src/lib/shell.mts +52 -0
  261. package/src/lib/time.mts +49 -0
  262. package/src/open-pr/README.md +237 -0
  263. package/src/open-pr/github.mts +444 -0
  264. package/src/open-pr/index.mts +5 -0
  265. package/src/open-pr/main.mts +327 -0
  266. package/src/open-pr/options.mts +101 -0
  267. package/src/open-pr/options.test.mts +67 -0
  268. package/src/open-pr/steps.mts +57 -0
  269. package/src/open-pr/steps.test.mts +59 -0
  270. package/src/open-pr/types.mts +42 -0
  271. package/src/pr-report/README.md +244 -0
  272. package/src/pr-report/github.mts +911 -0
  273. package/src/pr-report/index.mts +7 -0
  274. package/src/pr-report/linked-issues.mts +51 -0
  275. package/src/pr-report/linked-issues.test.mts +65 -0
  276. package/src/pr-report/main.mts +257 -0
  277. package/src/pr-report/options.mts +140 -0
  278. package/src/pr-report/queue.mts +125 -0
  279. package/src/pr-report/render.mts +428 -0
  280. package/src/pr-report/render.test.mts +379 -0
  281. package/src/pr-report/repo-settings.mts +34 -0
  282. package/src/push-fixed-pr/index.mts +1 -0
  283. package/src/push-fixed-pr/push-fixed-pr.mts +276 -0
  284. package/src/push-fixed-pr/push-fixed-pr.test.mts +228 -0
  285. package/src/report-unowned-prs/index.mts +1 -0
  286. package/src/report-unowned-prs/report-unowned-prs.mts +190 -0
  287. package/src/report-unowned-prs/report-unowned-prs.test.mts +87 -0
  288. package/src/unblock-prs/README.md +1616 -0
  289. package/src/unblock-prs/auto-fix.mts +334 -0
  290. package/src/unblock-prs/auto-fix.test.mts +159 -0
  291. package/src/unblock-prs/checks.mts +538 -0
  292. package/src/unblock-prs/checks.test.mts +507 -0
  293. package/src/unblock-prs/claims.mts +33 -0
  294. package/src/unblock-prs/claims.test.mts +64 -0
  295. package/src/unblock-prs/config.mts +187 -0
  296. package/src/unblock-prs/config.schema.json +71 -0
  297. package/src/unblock-prs/config.test.mts +139 -0
  298. package/src/unblock-prs/constants.mts +33 -0
  299. package/src/unblock-prs/demotions.mts +73 -0
  300. package/src/unblock-prs/demotions.test.mts +100 -0
  301. package/src/unblock-prs/github.mts +1042 -0
  302. package/src/unblock-prs/github.test.mts +84 -0
  303. package/src/unblock-prs/index.mts +24 -0
  304. package/src/unblock-prs/labels.mts +70 -0
  305. package/src/unblock-prs/main.mts +1051 -0
  306. package/src/unblock-prs/merge-after.mts +117 -0
  307. package/src/unblock-prs/merge-after.test.mts +120 -0
  308. package/src/unblock-prs/options.mts +157 -0
  309. package/src/unblock-prs/owner.mts +91 -0
  310. package/src/unblock-prs/owner.test.mts +114 -0
  311. package/src/unblock-prs/quiet.mts +62 -0
  312. package/src/unblock-prs/quiet.test.mts +175 -0
  313. package/src/unblock-prs/rebase.mts +962 -0
  314. package/src/unblock-prs/release.mts +149 -0
  315. package/src/unblock-prs/release.test.mts +209 -0
  316. package/src/unblock-prs/set-aside-comment.mts +143 -0
  317. package/src/unblock-prs/set-aside-comment.test.mts +148 -0
  318. package/src/unblock-prs/set-aside-detail.mts +82 -0
  319. package/src/unblock-prs/set-aside-detail.test.mts +131 -0
  320. package/src/unblock-prs/skips.mts +215 -0
  321. package/src/unblock-prs/skips.test.mts +280 -0
  322. package/src/unblock-prs/stack.mts +220 -0
  323. package/src/unblock-prs/stack.test.mts +449 -0
  324. package/src/unblock-prs/triage.mts +669 -0
  325. package/src/unblock-prs/triage.test.mts +100 -0
  326. package/src/unblock-prs/types.mts +358 -0
  327. package/src/unblock-prs/util.mts +78 -0
  328. package/src/unblock-prs/util.test.mts +26 -0
  329. package/src/unblock-prs/version-pr.mts +112 -0
  330. package/src/unblock-prs/version-pr.test.mts +78 -0
  331. package/src/unblock-prs/watch.mts +219 -0
@@ -0,0 +1,220 @@
1
+ // cspell:ignore retarget retargeted
2
+
3
+ /**
4
+ * Stacked pull requests: one whose base is another open pull request's
5
+ * branch rather than the default branch. Which pull request is on which is
6
+ * `pr-report-core`'s `stack.mts`, because the reports draw the same stacks;
7
+ * what this script does about them is decided here.
8
+ *
9
+ * The life of a layer, and what this script does at each point:
10
+ *
11
+ * 1. **Stacked.** Its base is the layer below, so its diff is its own and its
12
+ * turn has not come: it is reported as waiting, and nothing else. It has
13
+ * no auto-merge, and must not: nothing holds a pull request onto an
14
+ * unprotected branch, so armed it would land in the layer below rather
15
+ * than on the default branch. Nothing arms it — `open-pr` arms nothing,
16
+ * and this script arms a pull request only when it picks it
17
+ * (`armOnPick` in `rebase.mts`), which a stacked one never is.
18
+ * 2. **The layer below moves.** When this script rebases a pull request, it
19
+ * replays every layer stacked on it onto the new head with
20
+ * `git rebase --onto <new head> <old head>`, which carries exactly the
21
+ * layer's own commits. Otherwise each layer would show the old commits of
22
+ * the one below in its diff, and GitHub's own rebase, when that one
23
+ * merges, would start from a head the layer does not contain.
24
+ * 3. **The layer below merges.** GitHub moves this one onto the default
25
+ * branch. From then on it is an ordinary pull request, picked and armed
26
+ * in its turn like any other, save one thing: it still carries the merged
27
+ * layer's commits, so its diff shows that layer's changes as its own. The
28
+ * next survey rebases them off with `--onto`, from the head that layer
29
+ * merged at to its squash commit, and carries the layers above along —
30
+ * without waiting for it to be queued, because that is an undoing of
31
+ * GitHub's move, not a turn: it adds nothing from the base, and needs no
32
+ * review to have finished. The ruleset merges a branch only when it is up
33
+ * to date and only by squash, so that commit's tree is the merged head's,
34
+ * and the rebased layer's tree is the one it had: only its history, and
35
+ * so its diff, changes. A queued layer is left to its own rebase when it
36
+ * is picked, which drops the same commits the same way on its way onto
37
+ * the tip.
38
+ *
39
+ * A stack here is made by the base alone. GitHub's native stacks are not
40
+ * landed: GitHub refuses auto-merge on a pull request in one ("Auto-merge is
41
+ * not supported for stacked pull requests"), and keeps a layer in its stack
42
+ * after the one below has merged and it has been moved onto the default
43
+ * branch — so one is reported rather than picked, and merged by hand.
44
+ */
45
+
46
+ import {
47
+ findStackParents,
48
+ isSafeRefName,
49
+ type NativeStackEntry,
50
+ type QueueOwner,
51
+ } from 'pr-report-core';
52
+ import { isMergeQueued, isSkipCiLabelled, wantsAutoRebase } from './labels.mjs';
53
+ import type {
54
+ BaseChange,
55
+ BranchOf,
56
+ Classification,
57
+ PullRequest,
58
+ RetargetedLayer,
59
+ StackedOn,
60
+ TriageContext,
61
+ } from './types.mjs';
62
+ import { isVersionPullRequest } from './version-pr.mjs';
63
+
64
+ /**
65
+ * The branch the pull request was stacked on, if the last change of its base
66
+ * moved it off that branch and onto the default one.
67
+ */
68
+ export const retargetedFrom = (
69
+ changes: readonly BaseChange[],
70
+ defaultBranch: string,
71
+ ): string | undefined => {
72
+ const last = changes.at(-1);
73
+
74
+ return last?.to === defaultBranch && last.from !== defaultBranch
75
+ ? last.from
76
+ : undefined;
77
+ };
78
+
79
+ /** What to report for a stacked pull request in place of anything else. */
80
+ export const stackedNote = (
81
+ pr: PullRequest,
82
+ parent: number,
83
+ context: TriageContext,
84
+ ): Classification =>
85
+ ({
86
+ kind: 'note',
87
+ note: `#${pr.number}: stacked on #${parent}, which merges first${
88
+ context.cyclic.has(pr.number)
89
+ ? ' — and on a cycle of stacks and Merge-After, so nothing here will move it'
90
+ : ''
91
+ }`,
92
+ }) as const;
93
+
94
+ /**
95
+ * What to report instead of picking a pull request in one of GitHub's native
96
+ * stacks, which GitHub will not let this script arm.
97
+ */
98
+ export const nativeStackNote = (
99
+ pr: PullRequest,
100
+ entry: NativeStackEntry,
101
+ ): Classification =>
102
+ ({
103
+ kind: 'note',
104
+ note: `#${pr.number}: layer ${entry.position} of ${entry.size} of GitHub's native stack #${entry.stack}, on which GitHub refuses auto-merge; merge it by hand (a stack made by the base alone needs no native stack)`,
105
+ }) as const;
106
+
107
+ /**
108
+ * Why a layer cannot be carried along when the one below it moves, or
109
+ * `undefined` when it can. A fork's branch is not this repository's to push
110
+ * to, whatever its name.
111
+ */
112
+ export const restackable = (pr: BranchOf): string | undefined =>
113
+ pr.state !== 'OPEN'
114
+ ? (`it is ${pr.state}` as const)
115
+ : pr.isCrossRepository
116
+ ? 'its branch is in a fork'
117
+ : !isSafeRefName(pr.headRefName)
118
+ ? (`branch name ${JSON.stringify(pr.headRefName)} will not be passed to a shell` as const)
119
+ : undefined;
120
+
121
+ /**
122
+ * The pull requests this script rebases onto the default branch ahead of any
123
+ * turn: the bottom layer of a stack, or a lone pull request, that asked for it
124
+ * (`wantsAutoRebase`) and is paused by `skip-ci`, so the push costs no
125
+ * matrix. Whether it is actually behind is for the caller to ask
126
+ * git: `skip-ci` leaves the merge state at `BLOCKED` whatever the branch is.
127
+ * The version pull request is the release workflow's. Only the worker's own
128
+ * are moved (`owner.mts`): one set aside or claimed is a skill's or a
129
+ * person's until its comment is resolved or the claim ends.
130
+ */
131
+ export const autoRebaseTargets = (
132
+ pullRequests: readonly PullRequest[],
133
+ defaultBranch: string,
134
+ {
135
+ owners,
136
+ }: Readonly<{
137
+ /** Who moves each queued pull request next (`owner.mts`). */
138
+ owners: ReadonlyMap<number, QueueOwner>;
139
+ }>,
140
+ ): readonly PullRequest[] =>
141
+ pullRequests.filter(
142
+ (pr) =>
143
+ pr.baseRefName === defaultBranch &&
144
+ !pr.isDraft &&
145
+ wantsAutoRebase(pr) &&
146
+ isSkipCiLabelled(pr) &&
147
+ !isVersionPullRequest(pr, defaultBranch) &&
148
+ restackable(pr) === undefined &&
149
+ owners.get(pr.number)?.owner === 'worker',
150
+ );
151
+
152
+ /** Which open pull request each stacked one is on: child → parent. */
153
+ export const stackParentsOf = (
154
+ pullRequests: readonly PullRequest[],
155
+ defaultBranch: string,
156
+ ): ReadonlyMap<number, number> =>
157
+ findStackParents(
158
+ pullRequests.map((pr) => ({
159
+ number: pr.number,
160
+ headRef: pr.headRefName,
161
+ baseRef: pr.baseRefName,
162
+ fromFork: pr.isCrossRepository,
163
+ })),
164
+ defaultBranch,
165
+ );
166
+
167
+ /**
168
+ * What to remember of this survey's stacks for the next one: every layer, by
169
+ * the branch it is on, and every layer remembered before that is still on
170
+ * that branch. The second is a layer whose parent has merged while GitHub
171
+ * has yet to move it — no open pull request heads its base any longer, so
172
+ * the bases alone no longer say it is a layer, but it has still to come off
173
+ * one.
174
+ */
175
+ export const stackedOnAfter = (
176
+ before: StackedOn | undefined,
177
+ pullRequests: readonly PullRequest[],
178
+ stackParents: ReadonlyMap<number, number>,
179
+ ): StackedOn =>
180
+ new Map(
181
+ pullRequests
182
+ .filter(
183
+ (pr) =>
184
+ stackParents.has(pr.number) ||
185
+ before?.get(pr.number) === pr.baseRefName,
186
+ )
187
+ .map((pr) => [pr.number, pr.baseRefName] as const),
188
+ );
189
+
190
+ /**
191
+ * The pull requests the last survey saw stacked and this one sees on the
192
+ * default branch — before the first survey, every pull request on the
193
+ * default branch, whose timeline then says whether it came off a stack.
194
+ *
195
+ * A queued one is not among them: its own rebase, when it is picked, drops
196
+ * the merged layer's commits on its way onto the tip. Nor is anything that
197
+ * cannot be pushed to, or the version pull request, which was never a layer.
198
+ */
199
+ export const retargetedLayers = (
200
+ before: StackedOn | undefined,
201
+ pullRequests: readonly PullRequest[],
202
+ defaultBranch: string,
203
+ ): readonly RetargetedLayer[] =>
204
+ pullRequests
205
+ .filter(
206
+ (pr) =>
207
+ pr.baseRefName === defaultBranch &&
208
+ !isMergeQueued(pr) &&
209
+ !isVersionPullRequest(pr, defaultBranch) &&
210
+ restackable(pr) === undefined,
211
+ )
212
+ .flatMap((pr): readonly RetargetedLayer[] => {
213
+ if (before === undefined) {
214
+ return [{ pr, from: undefined }];
215
+ }
216
+
217
+ const from = before.get(pr.number);
218
+
219
+ return from === undefined ? [] : [{ pr, from }];
220
+ });
@@ -0,0 +1,449 @@
1
+ // cspell:ignore retarget retargeted
2
+
3
+ import type { Claim } from 'pr-report-core';
4
+ import { Arr } from 'ts-data-forge';
5
+ import { ownerOf } from './owner.mjs';
6
+ import {
7
+ autoRebaseTargets,
8
+ nativeStackNote,
9
+ restackable,
10
+ retargetedFrom,
11
+ retargetedLayers,
12
+ stackedNote,
13
+ stackedOnAfter,
14
+ stackParentsOf,
15
+ } from './stack.mjs';
16
+ import type {
17
+ BaseChange,
18
+ PullRequest,
19
+ SkipRecord,
20
+ TriageContext,
21
+ } from './types.mjs';
22
+
23
+ const NOW = Date.parse('2026-10-15T00:30:00Z');
24
+
25
+ const pullRequest = (
26
+ fields: Partial<PullRequest> & Readonly<{ number: number }>,
27
+ ): PullRequest =>
28
+ ({
29
+ id: `PR_${fields.number}`,
30
+ title: `pull request #${fields.number}`,
31
+ body: '',
32
+ state: 'OPEN',
33
+ headRefName: `feature/${fields.number}`,
34
+ headRefOid: 'a'.repeat(40),
35
+ baseRefName: 'main',
36
+ isCrossRepository: false,
37
+ isDraft: false,
38
+ mergeStateStatus: 'BLOCKED',
39
+ autoMergeRequest: null,
40
+ labels: [],
41
+ ...fields,
42
+ }) as const;
43
+
44
+ const context = (
45
+ stackParents: ReadonlyMap<number, number>,
46
+ cyclic: ReadonlySet<number> = new Set(),
47
+ ): TriageContext =>
48
+ ({
49
+ defaultBranch: 'main',
50
+ baseSha: 'b'.repeat(40),
51
+ skipped: new Map(),
52
+ claimed: new Map(),
53
+ owners: new Map(),
54
+ demoted: new Map(),
55
+ requiredContexts: [],
56
+ issueOrder: { closes: new Map(), startAfter: new Map() },
57
+ reviewRequirements: {
58
+ requireCodeOwnerReview: false,
59
+ requireConversationResolution: false,
60
+ },
61
+ openNumbers: new Set(),
62
+ dependencies: new Map(),
63
+ inherited: new Map(),
64
+ stackParents,
65
+ cycles: [],
66
+ cyclic,
67
+ releaseBlockers: [],
68
+ }) as const;
69
+
70
+ const retarget = (from: string, to = 'main'): BaseChange =>
71
+ ({ from, to }) as const;
72
+
73
+ describe(retargetedFrom, () => {
74
+ test('names the branch the last change of base moved it off', () => {
75
+ assert.strictEqual(
76
+ retargetedFrom([retarget('feature/1')], 'main'),
77
+ 'feature/1',
78
+ );
79
+ });
80
+
81
+ test('is nothing for a pull request that never changed base', () => {
82
+ assert.isUndefined(retargetedFrom([], 'main'));
83
+ });
84
+
85
+ test('is nothing when the last change was away from the default branch', () => {
86
+ assert.isUndefined(
87
+ retargetedFrom(
88
+ [retarget('feature/1'), retarget('main', 'feature/2')],
89
+ 'main',
90
+ ),
91
+ );
92
+ });
93
+
94
+ test('reads the last change of base, not the first', () => {
95
+ assert.strictEqual(
96
+ retargetedFrom(
97
+ [retarget('feature/1', 'feature/2'), retarget('feature/2')],
98
+ 'main',
99
+ ),
100
+ 'feature/2',
101
+ );
102
+ });
103
+ });
104
+
105
+ describe(stackedNote, () => {
106
+ test('says which layer it waits for', () => {
107
+ assert.deepStrictEqual(
108
+ stackedNote(pullRequest({ number: 2 }), 1, context(new Map([[2, 1]]))),
109
+ {
110
+ kind: 'note',
111
+ note: '#2: stacked on #1, which merges first',
112
+ },
113
+ );
114
+ });
115
+
116
+ test('says when the stack loops back on itself', () => {
117
+ const note = stackedNote(
118
+ pullRequest({ number: 2 }),
119
+ 1,
120
+ context(
121
+ new Map([
122
+ [2, 1],
123
+ [1, 2],
124
+ ]),
125
+ new Set([1, 2]),
126
+ ),
127
+ );
128
+
129
+ assert.isTrue(note.kind === 'note' && note.note.includes('cycle'));
130
+ });
131
+ });
132
+
133
+ describe(restackable, () => {
134
+ test('keeps a layer this repository can push to', () => {
135
+ assert.isUndefined(restackable(pullRequest({ number: 2 })));
136
+ });
137
+
138
+ test("refuses a fork's branch", () => {
139
+ assert.isDefined(
140
+ restackable(pullRequest({ number: 2, isCrossRepository: true })),
141
+ );
142
+ });
143
+
144
+ test('refuses a branch name it will not pass to a shell', () => {
145
+ assert.isDefined(
146
+ restackable(pullRequest({ number: 2, headRefName: "it's" })),
147
+ );
148
+ });
149
+
150
+ test('refuses one that is not open', () => {
151
+ assert.isDefined(restackable(pullRequest({ number: 2, state: 'CLOSED' })));
152
+ });
153
+ });
154
+
155
+ describe(nativeStackNote, () => {
156
+ test('says why one in a native stack is not armed, and what to do', () => {
157
+ // GitHub keeps the layer in the stack after the one below it merged and
158
+ // it was moved onto `main`, and refuses auto-merge for as long.
159
+ assert.deepStrictEqual(
160
+ nativeStackNote(pullRequest({ number: 2 }), {
161
+ stack: 3,
162
+ position: 2,
163
+ size: 2,
164
+ }),
165
+ {
166
+ kind: 'note',
167
+ note: "#2: layer 2 of 2 of GitHub's native stack #3, on which GitHub refuses auto-merge; merge it by hand (a stack made by the base alone needs no native stack)",
168
+ },
169
+ );
170
+ });
171
+ });
172
+
173
+ describe(stackParentsOf, () => {
174
+ test('reads which open pull request each layer is on from the bases', () => {
175
+ assert.deepStrictEqual(
176
+ stackParentsOf(
177
+ [
178
+ pullRequest({ number: 1 }),
179
+ pullRequest({ number: 2, baseRefName: 'feature/1' }),
180
+ pullRequest({ number: 3, baseRefName: 'feature/2' }),
181
+ ],
182
+ 'main',
183
+ ),
184
+ new Map([
185
+ [2, 1],
186
+ [3, 2],
187
+ ]),
188
+ );
189
+ });
190
+ });
191
+
192
+ describe(stackedOnAfter, () => {
193
+ test('remembers the branch each layer is stacked on', () => {
194
+ const pulls = [
195
+ pullRequest({ number: 1 }),
196
+ pullRequest({ number: 2, baseRefName: 'feature/1' }),
197
+ ] as const;
198
+
199
+ assert.deepStrictEqual(
200
+ stackedOnAfter(undefined, pulls, stackParentsOf(pulls, 'main')),
201
+ new Map([[2, 'feature/1']]),
202
+ );
203
+ });
204
+
205
+ test('keeps a layer whose parent merged until GitHub moves it', () => {
206
+ // #1 merged and left the open list, and GitHub has not moved #2 yet: the
207
+ // bases no longer make a stack, but #2 has still to come off one.
208
+ const pulls = [
209
+ pullRequest({ number: 2, baseRefName: 'feature/1' }),
210
+ ] as const;
211
+
212
+ assert.deepStrictEqual(
213
+ stackedOnAfter(
214
+ new Map([[2, 'feature/1']]),
215
+ pulls,
216
+ stackParentsOf(pulls, 'main'),
217
+ ),
218
+ new Map([[2, 'feature/1']]),
219
+ );
220
+ });
221
+
222
+ test('forgets a layer once it is on another base', () => {
223
+ const pulls = [pullRequest({ number: 2 })] as const;
224
+
225
+ assert.deepStrictEqual(
226
+ stackedOnAfter(
227
+ new Map([[2, 'feature/1']]),
228
+ pulls,
229
+ stackParentsOf(pulls, 'main'),
230
+ ),
231
+ new Map(),
232
+ );
233
+ });
234
+
235
+ test('forgets a layer that is no longer open', () => {
236
+ assert.deepStrictEqual(
237
+ stackedOnAfter(new Map([[2, 'feature/1']]), [], new Map()),
238
+ new Map(),
239
+ );
240
+ });
241
+ });
242
+
243
+ describe(retargetedLayers, () => {
244
+ test('names a layer that was stacked and is now on the default branch', () => {
245
+ const layer = pullRequest({ number: 2 });
246
+
247
+ assert.deepStrictEqual(
248
+ retargetedLayers(new Map([[2, 'feature/1']]), [layer], 'main'),
249
+ [{ pr: layer, from: 'feature/1' }],
250
+ );
251
+ });
252
+
253
+ test('is nothing for a layer GitHub has not moved yet', () => {
254
+ assert.deepStrictEqual(
255
+ retargetedLayers(
256
+ new Map([[2, 'feature/1']]),
257
+ [pullRequest({ number: 2, baseRefName: 'feature/1' })],
258
+ 'main',
259
+ ),
260
+ [],
261
+ );
262
+ });
263
+
264
+ test('is nothing for a pull request that was never seen stacked', () => {
265
+ assert.deepStrictEqual(
266
+ retargetedLayers(new Map(), [pullRequest({ number: 2 })], 'main'),
267
+ [],
268
+ );
269
+ });
270
+
271
+ test('before the first survey, offers every pull request on the default branch, with the branch to be read from its timeline', () => {
272
+ const layer = pullRequest({ number: 2 });
273
+
274
+ assert.deepStrictEqual(
275
+ retargetedLayers(
276
+ undefined,
277
+ [layer, pullRequest({ number: 3, baseRefName: 'feature/2' })],
278
+ 'main',
279
+ ),
280
+ [{ pr: layer, from: undefined }],
281
+ );
282
+ });
283
+
284
+ test('leaves a queued pull request to its own rebase when it is picked', () => {
285
+ assert.deepStrictEqual(
286
+ retargetedLayers(
287
+ new Map([[2, 'feature/1']]),
288
+ [pullRequest({ number: 2, labels: [{ name: 'merge-queued' }] })],
289
+ 'main',
290
+ ),
291
+ [],
292
+ );
293
+ });
294
+
295
+ test("leaves a fork's branch and the version pull request alone", () => {
296
+ assert.deepStrictEqual(
297
+ retargetedLayers(
298
+ undefined,
299
+ [
300
+ pullRequest({ number: 2, isCrossRepository: true }),
301
+ pullRequest({ number: 3, headRefName: 'changeset-release/main' }),
302
+ ],
303
+ 'main',
304
+ ),
305
+ [],
306
+ );
307
+ });
308
+ });
309
+
310
+ const labelled = (...names: readonly string[]): PullRequest['labels'] =>
311
+ names.map((name) => ({ name }));
312
+
313
+ const targets = (
314
+ pullRequests: readonly PullRequest[],
315
+ skipped: ReadonlyMap<number, SkipRecord> = new Map(),
316
+ claimed: ReadonlyMap<number, Claim> = new Map(),
317
+ ): readonly number[] =>
318
+ autoRebaseTargets(pullRequests, 'main', {
319
+ owners: new Map(
320
+ Arr.generate(function* () {
321
+ for (const pr of pullRequests) {
322
+ const owner = ownerOf(pr, {
323
+ defaultBranch: 'main',
324
+ skipped,
325
+ claimed,
326
+ stackParents: new Map(),
327
+ cyclic: new Set(),
328
+ nowEpochMs: NOW,
329
+ });
330
+
331
+ if (owner !== undefined) {
332
+ yield [pr.number, owner] as const;
333
+ }
334
+ }
335
+ }),
336
+ ),
337
+ }).map((pr) => pr.number);
338
+
339
+ describe(autoRebaseTargets, () => {
340
+ test('takes a paused pull request labelled auto-rebase, and a paused queued one', () => {
341
+ assert.deepStrictEqual(
342
+ targets([
343
+ pullRequest({ number: 1, labels: labelled('skip-ci', 'auto-rebase') }),
344
+ pullRequest({ number: 2, labels: labelled('skip-ci', 'merge-queued') }),
345
+ ]),
346
+ [1, 2],
347
+ );
348
+ });
349
+
350
+ test('leaves a paused pull request that asked for neither', () => {
351
+ assert.deepStrictEqual(
352
+ targets([pullRequest({ number: 1, labels: labelled('skip-ci') })]),
353
+ [],
354
+ );
355
+ });
356
+
357
+ test('leaves one that skip-ci is not on, since a push would run the matrix', () => {
358
+ assert.deepStrictEqual(
359
+ targets([
360
+ pullRequest({ number: 1, labels: labelled('auto-rebase') }),
361
+ pullRequest({ number: 2, labels: labelled('merge-queued') }),
362
+ ]),
363
+ [],
364
+ );
365
+ });
366
+
367
+ test('leaves a draft, a layer, a fork and the version pull request', () => {
368
+ assert.deepStrictEqual(
369
+ targets([
370
+ pullRequest({
371
+ number: 1,
372
+ isDraft: true,
373
+ labels: labelled('skip-ci', 'auto-rebase'),
374
+ }),
375
+ pullRequest({
376
+ number: 2,
377
+ baseRefName: 'feature/1',
378
+ labels: labelled('skip-ci', 'auto-rebase'),
379
+ }),
380
+ pullRequest({
381
+ number: 3,
382
+ isCrossRepository: true,
383
+ labels: labelled('skip-ci', 'auto-rebase'),
384
+ }),
385
+ pullRequest({
386
+ number: 4,
387
+ headRefName: 'changeset-release/main',
388
+ labels: labelled('skip-ci', 'merge-queued'),
389
+ }),
390
+ ]),
391
+ [],
392
+ );
393
+ });
394
+
395
+ test('leaves one set aside at its head, whatever the base', () => {
396
+ const given = pullRequest({
397
+ number: 1,
398
+ labels: labelled('skip-ci', 'auto-rebase'),
399
+ });
400
+
401
+ assert.deepStrictEqual(
402
+ targets(
403
+ [given],
404
+ new Map([
405
+ [
406
+ 1,
407
+ {
408
+ number: 1,
409
+ headSha: given.headRefOid,
410
+ baseSha: 'b'.repeat(40),
411
+ reason: 'rebase-failed',
412
+ detail: 'conflict',
413
+ },
414
+ ],
415
+ ]),
416
+ ),
417
+ [],
418
+ );
419
+ });
420
+
421
+ test('leaves one a session has claimed, until its claim ends', () => {
422
+ assert.deepStrictEqual(
423
+ targets(
424
+ [
425
+ pullRequest({
426
+ number: 1,
427
+ labels: labelled('skip-ci', 'auto-rebase'),
428
+ }),
429
+ pullRequest({
430
+ number: 2,
431
+ labels: labelled('skip-ci', 'auto-rebase'),
432
+ }),
433
+ ],
434
+ new Map(),
435
+ new Map([
436
+ [
437
+ 1,
438
+ {
439
+ headSha: pullRequest({ number: 1 }).headRefOid,
440
+ by: 'unblock-wren',
441
+ at: '2026-10-15T00:00:00Z',
442
+ },
443
+ ],
444
+ ]),
445
+ ),
446
+ [2],
447
+ );
448
+ });
449
+ });