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.
- package/LICENSE +201 -0
- package/README.md +125 -2
- package/dist/claim-pr/claim-pr.d.mts +45 -0
- package/dist/claim-pr/claim-pr.d.mts.map +1 -0
- package/dist/claim-pr/claim-pr.mjs +217 -0
- package/dist/claim-pr/claim-pr.mjs.map +1 -0
- package/dist/claim-pr/index.d.mts +2 -0
- package/dist/claim-pr/index.d.mts.map +1 -0
- package/dist/claim-pr/index.mjs +2 -0
- package/dist/claim-pr/index.mjs.map +1 -0
- package/dist/cmd/claim-pr.d.mts +3 -0
- package/dist/cmd/claim-pr.d.mts.map +1 -0
- package/dist/cmd/claim-pr.mjs +20 -0
- package/dist/cmd/claim-pr.mjs.map +1 -0
- package/dist/cmd/open-pr.d.mts +3 -0
- package/dist/cmd/open-pr.d.mts.map +1 -0
- package/dist/cmd/open-pr.mjs +20 -0
- package/dist/cmd/open-pr.mjs.map +1 -0
- package/dist/cmd/pr-report.d.mts +3 -0
- package/dist/cmd/pr-report.d.mts.map +1 -0
- package/dist/cmd/pr-report.mjs +20 -0
- package/dist/cmd/pr-report.mjs.map +1 -0
- package/dist/cmd/push-fixed-pr.d.mts +3 -0
- package/dist/cmd/push-fixed-pr.d.mts.map +1 -0
- package/dist/cmd/push-fixed-pr.mjs +20 -0
- package/dist/cmd/push-fixed-pr.mjs.map +1 -0
- package/dist/cmd/report-unowned-prs.d.mts +3 -0
- package/dist/cmd/report-unowned-prs.d.mts.map +1 -0
- package/dist/cmd/report-unowned-prs.mjs +20 -0
- package/dist/cmd/report-unowned-prs.mjs.map +1 -0
- package/dist/cmd/unblock-prs.d.mts +3 -0
- package/dist/cmd/unblock-prs.d.mts.map +1 -0
- package/dist/cmd/unblock-prs.mjs +19 -0
- package/dist/cmd/unblock-prs.mjs.map +1 -0
- package/dist/entry-point.d.mts +3 -0
- package/dist/entry-point.d.mts.map +1 -0
- package/dist/entry-point.mjs +3 -0
- package/dist/entry-point.mjs.map +1 -0
- package/dist/lib/cli-args.d.mts +27 -0
- package/dist/lib/cli-args.d.mts.map +1 -0
- package/dist/lib/cli-args.mjs +55 -0
- package/dist/lib/cli-args.mjs.map +1 -0
- package/dist/lib/github-api.d.mts +153 -0
- package/dist/lib/github-api.d.mts.map +1 -0
- package/dist/lib/github-api.mjs +291 -0
- package/dist/lib/github-api.mjs.map +1 -0
- package/dist/lib/index.d.mts +7 -0
- package/dist/lib/index.d.mts.map +1 -0
- package/dist/lib/index.mjs +7 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/json.d.mts +12 -0
- package/dist/lib/json.d.mts.map +1 -0
- package/dist/lib/json.mjs +28 -0
- package/dist/lib/json.mjs.map +1 -0
- package/dist/lib/log.d.mts +10 -0
- package/dist/lib/log.d.mts.map +1 -0
- package/dist/lib/log.mjs +13 -0
- package/dist/lib/log.mjs.map +1 -0
- package/dist/lib/shell.d.mts +18 -0
- package/dist/lib/shell.d.mts.map +1 -0
- package/dist/lib/shell.mjs +42 -0
- package/dist/lib/shell.mjs.map +1 -0
- package/dist/lib/time.d.mts +17 -0
- package/dist/lib/time.d.mts.map +1 -0
- package/dist/lib/time.mjs +35 -0
- package/dist/lib/time.mjs.map +1 -0
- package/dist/open-pr/github.d.mts +72 -0
- package/dist/open-pr/github.d.mts.map +1 -0
- package/dist/open-pr/github.mjs +249 -0
- package/dist/open-pr/github.mjs.map +1 -0
- package/dist/open-pr/index.d.mts +6 -0
- package/dist/open-pr/index.d.mts.map +1 -0
- package/dist/open-pr/index.mjs +6 -0
- package/dist/open-pr/index.mjs.map +1 -0
- package/dist/open-pr/main.d.mts +36 -0
- package/dist/open-pr/main.d.mts.map +1 -0
- package/dist/open-pr/main.mjs +204 -0
- package/dist/open-pr/main.mjs.map +1 -0
- package/dist/open-pr/options.d.mts +20 -0
- package/dist/open-pr/options.d.mts.map +1 -0
- package/dist/open-pr/options.mjs +66 -0
- package/dist/open-pr/options.mjs.map +1 -0
- package/dist/open-pr/steps.d.mts +23 -0
- package/dist/open-pr/steps.d.mts.map +1 -0
- package/dist/open-pr/steps.mjs +38 -0
- package/dist/open-pr/steps.mjs.map +1 -0
- package/dist/open-pr/types.d.mts +179 -0
- package/dist/open-pr/types.d.mts.map +1 -0
- package/dist/open-pr/types.mjs +20 -0
- package/dist/open-pr/types.mjs.map +1 -0
- package/dist/pr-report/github.d.mts +59 -0
- package/dist/pr-report/github.d.mts.map +1 -0
- package/dist/pr-report/github.mjs +561 -0
- package/dist/pr-report/github.mjs.map +1 -0
- package/dist/pr-report/index.d.mts +8 -0
- package/dist/pr-report/index.d.mts.map +1 -0
- package/dist/pr-report/index.mjs +8 -0
- package/dist/pr-report/index.mjs.map +1 -0
- package/dist/pr-report/linked-issues.d.mts +15 -0
- package/dist/pr-report/linked-issues.d.mts.map +1 -0
- package/dist/pr-report/linked-issues.mjs +39 -0
- package/dist/pr-report/linked-issues.mjs.map +1 -0
- package/dist/pr-report/main.d.mts +42 -0
- package/dist/pr-report/main.d.mts.map +1 -0
- package/dist/pr-report/main.mjs +163 -0
- package/dist/pr-report/main.mjs.map +1 -0
- package/dist/pr-report/options.d.mts +36 -0
- package/dist/pr-report/options.d.mts.map +1 -0
- package/dist/pr-report/options.mjs +90 -0
- package/dist/pr-report/options.mjs.map +1 -0
- package/dist/pr-report/queue.d.mts +28 -0
- package/dist/pr-report/queue.d.mts.map +1 -0
- package/dist/pr-report/queue.mjs +74 -0
- package/dist/pr-report/queue.mjs.map +1 -0
- package/dist/pr-report/render.d.mts +14 -0
- package/dist/pr-report/render.d.mts.map +1 -0
- package/dist/pr-report/render.mjs +318 -0
- package/dist/pr-report/render.mjs.map +1 -0
- package/dist/pr-report/repo-settings.d.mts +12 -0
- package/dist/pr-report/repo-settings.d.mts.map +1 -0
- package/dist/pr-report/repo-settings.mjs +22 -0
- package/dist/pr-report/repo-settings.mjs.map +1 -0
- package/dist/push-fixed-pr/index.d.mts +2 -0
- package/dist/push-fixed-pr/index.d.mts.map +1 -0
- package/dist/push-fixed-pr/index.mjs +2 -0
- package/dist/push-fixed-pr/index.mjs.map +1 -0
- package/dist/push-fixed-pr/push-fixed-pr.d.mts +54 -0
- package/dist/push-fixed-pr/push-fixed-pr.d.mts.map +1 -0
- package/dist/push-fixed-pr/push-fixed-pr.mjs +172 -0
- package/dist/push-fixed-pr/push-fixed-pr.mjs.map +1 -0
- package/dist/report-unowned-prs/index.d.mts +2 -0
- package/dist/report-unowned-prs/index.d.mts.map +1 -0
- package/dist/report-unowned-prs/index.mjs +2 -0
- package/dist/report-unowned-prs/index.mjs.map +1 -0
- package/dist/report-unowned-prs/report-unowned-prs.d.mts +49 -0
- package/dist/report-unowned-prs/report-unowned-prs.d.mts.map +1 -0
- package/dist/report-unowned-prs/report-unowned-prs.mjs +127 -0
- package/dist/report-unowned-prs/report-unowned-prs.mjs.map +1 -0
- package/dist/tsconfig.json +1 -0
- package/dist/unblock-prs/auto-fix.d.mts +45 -0
- package/dist/unblock-prs/auto-fix.d.mts.map +1 -0
- package/dist/unblock-prs/auto-fix.mjs +207 -0
- package/dist/unblock-prs/auto-fix.mjs.map +1 -0
- package/dist/unblock-prs/checks.d.mts +307 -0
- package/dist/unblock-prs/checks.d.mts.map +1 -0
- package/dist/unblock-prs/checks.mjs +372 -0
- package/dist/unblock-prs/checks.mjs.map +1 -0
- package/dist/unblock-prs/claims.d.mts +21 -0
- package/dist/unblock-prs/claims.d.mts.map +1 -0
- package/dist/unblock-prs/claims.mjs +23 -0
- package/dist/unblock-prs/claims.mjs.map +1 -0
- package/dist/unblock-prs/config.d.mts +487 -0
- package/dist/unblock-prs/config.d.mts.map +1 -0
- package/dist/unblock-prs/config.mjs +119 -0
- package/dist/unblock-prs/config.mjs.map +1 -0
- package/dist/unblock-prs/constants.d.mts +28 -0
- package/dist/unblock-prs/constants.d.mts.map +1 -0
- package/dist/unblock-prs/constants.mjs +28 -0
- package/dist/unblock-prs/constants.mjs.map +1 -0
- package/dist/unblock-prs/demotions.d.mts +42 -0
- package/dist/unblock-prs/demotions.d.mts.map +1 -0
- package/dist/unblock-prs/demotions.mjs +50 -0
- package/dist/unblock-prs/demotions.mjs.map +1 -0
- package/dist/unblock-prs/github.d.mts +739 -0
- package/dist/unblock-prs/github.d.mts.map +1 -0
- package/dist/unblock-prs/github.mjs +626 -0
- package/dist/unblock-prs/github.mjs.map +1 -0
- package/dist/unblock-prs/index.d.mts +25 -0
- package/dist/unblock-prs/index.d.mts.map +1 -0
- package/dist/unblock-prs/index.mjs +25 -0
- package/dist/unblock-prs/index.mjs.map +1 -0
- package/dist/unblock-prs/labels.d.mts +49 -0
- package/dist/unblock-prs/labels.d.mts.map +1 -0
- package/dist/unblock-prs/labels.mjs +49 -0
- package/dist/unblock-prs/labels.mjs.map +1 -0
- package/dist/unblock-prs/main.d.mts +179 -0
- package/dist/unblock-prs/main.d.mts.map +1 -0
- package/dist/unblock-prs/main.mjs +640 -0
- package/dist/unblock-prs/main.mjs.map +1 -0
- package/dist/unblock-prs/merge-after.d.mts +46 -0
- package/dist/unblock-prs/merge-after.d.mts.map +1 -0
- package/dist/unblock-prs/merge-after.mjs +65 -0
- package/dist/unblock-prs/merge-after.mjs.map +1 -0
- package/dist/unblock-prs/options.d.mts +35 -0
- package/dist/unblock-prs/options.d.mts.map +1 -0
- package/dist/unblock-prs/options.mjs +88 -0
- package/dist/unblock-prs/options.mjs.map +1 -0
- package/dist/unblock-prs/owner.d.mts +32 -0
- package/dist/unblock-prs/owner.d.mts.map +1 -0
- package/dist/unblock-prs/owner.mjs +63 -0
- package/dist/unblock-prs/owner.mjs.map +1 -0
- package/dist/unblock-prs/quiet.d.mts +21 -0
- package/dist/unblock-prs/quiet.d.mts.map +1 -0
- package/dist/unblock-prs/quiet.mjs +42 -0
- package/dist/unblock-prs/quiet.mjs.map +1 -0
- package/dist/unblock-prs/rebase.d.mts +120 -0
- package/dist/unblock-prs/rebase.d.mts.map +1 -0
- package/dist/unblock-prs/rebase.mjs +576 -0
- package/dist/unblock-prs/rebase.mjs.map +1 -0
- package/dist/unblock-prs/release.d.mts +62 -0
- package/dist/unblock-prs/release.d.mts.map +1 -0
- package/dist/unblock-prs/release.mjs +94 -0
- package/dist/unblock-prs/release.mjs.map +1 -0
- package/dist/unblock-prs/set-aside-comment.d.mts +20 -0
- package/dist/unblock-prs/set-aside-comment.d.mts.map +1 -0
- package/dist/unblock-prs/set-aside-comment.mjs +93 -0
- package/dist/unblock-prs/set-aside-comment.mjs.map +1 -0
- package/dist/unblock-prs/set-aside-detail.d.mts +41 -0
- package/dist/unblock-prs/set-aside-detail.d.mts.map +1 -0
- package/dist/unblock-prs/set-aside-detail.mjs +56 -0
- package/dist/unblock-prs/set-aside-detail.mjs.map +1 -0
- package/dist/unblock-prs/skips.d.mts +85 -0
- package/dist/unblock-prs/skips.d.mts.map +1 -0
- package/dist/unblock-prs/skips.mjs +141 -0
- package/dist/unblock-prs/skips.mjs.map +1 -0
- package/dist/unblock-prs/stack.d.mts +98 -0
- package/dist/unblock-prs/stack.d.mts.map +1 -0
- package/dist/unblock-prs/stack.mjs +141 -0
- package/dist/unblock-prs/stack.mjs.map +1 -0
- package/dist/unblock-prs/triage.d.mts +28 -0
- package/dist/unblock-prs/triage.d.mts.map +1 -0
- package/dist/unblock-prs/triage.mjs +396 -0
- package/dist/unblock-prs/triage.mjs.map +1 -0
- package/dist/unblock-prs/types.d.mts +793 -0
- package/dist/unblock-prs/types.d.mts.map +1 -0
- package/dist/unblock-prs/types.mjs +26 -0
- package/dist/unblock-prs/types.mjs.map +1 -0
- package/dist/unblock-prs/util.d.mts +23 -0
- package/dist/unblock-prs/util.d.mts.map +1 -0
- package/dist/unblock-prs/util.mjs +60 -0
- package/dist/unblock-prs/util.mjs.map +1 -0
- package/dist/unblock-prs/version-pr.d.mts +57 -0
- package/dist/unblock-prs/version-pr.d.mts.map +1 -0
- package/dist/unblock-prs/version-pr.mjs +79 -0
- package/dist/unblock-prs/version-pr.mjs.map +1 -0
- package/dist/unblock-prs/watch.d.mts +17 -0
- package/dist/unblock-prs/watch.d.mts.map +1 -0
- package/dist/unblock-prs/watch.mjs +128 -0
- package/dist/unblock-prs/watch.mjs.map +1 -0
- package/package.json +118 -3
- package/src/claim-pr/claim-pr.mts +380 -0
- package/src/claim-pr/claim-pr.test.mts +176 -0
- package/src/claim-pr/index.mts +1 -0
- package/src/cmd/claim-pr.mts +32 -0
- package/src/cmd/open-pr.mts +28 -0
- package/src/cmd/pr-report.mts +28 -0
- package/src/cmd/push-fixed-pr.mts +36 -0
- package/src/cmd/report-unowned-prs.mts +37 -0
- package/src/cmd/unblock-prs.mts +24 -0
- package/src/entry-point.mts +3 -0
- package/src/lib/cli-args.mts +86 -0
- package/src/lib/cli-args.test.mts +92 -0
- package/src/lib/github-api.mts +463 -0
- package/src/lib/github-api.test.mts +40 -0
- package/src/lib/index.mts +6 -0
- package/src/lib/json.mts +43 -0
- package/src/lib/json.test.mts +34 -0
- package/src/lib/log.mts +19 -0
- package/src/lib/log.test.mts +45 -0
- package/src/lib/shell.mts +52 -0
- package/src/lib/time.mts +49 -0
- package/src/open-pr/README.md +237 -0
- package/src/open-pr/github.mts +444 -0
- package/src/open-pr/index.mts +5 -0
- package/src/open-pr/main.mts +327 -0
- package/src/open-pr/options.mts +101 -0
- package/src/open-pr/options.test.mts +67 -0
- package/src/open-pr/steps.mts +57 -0
- package/src/open-pr/steps.test.mts +59 -0
- package/src/open-pr/types.mts +42 -0
- package/src/pr-report/README.md +244 -0
- package/src/pr-report/github.mts +911 -0
- package/src/pr-report/index.mts +7 -0
- package/src/pr-report/linked-issues.mts +51 -0
- package/src/pr-report/linked-issues.test.mts +65 -0
- package/src/pr-report/main.mts +257 -0
- package/src/pr-report/options.mts +140 -0
- package/src/pr-report/queue.mts +125 -0
- package/src/pr-report/render.mts +428 -0
- package/src/pr-report/render.test.mts +379 -0
- package/src/pr-report/repo-settings.mts +34 -0
- package/src/push-fixed-pr/index.mts +1 -0
- package/src/push-fixed-pr/push-fixed-pr.mts +276 -0
- package/src/push-fixed-pr/push-fixed-pr.test.mts +228 -0
- package/src/report-unowned-prs/index.mts +1 -0
- package/src/report-unowned-prs/report-unowned-prs.mts +190 -0
- package/src/report-unowned-prs/report-unowned-prs.test.mts +87 -0
- package/src/unblock-prs/README.md +1616 -0
- package/src/unblock-prs/auto-fix.mts +334 -0
- package/src/unblock-prs/auto-fix.test.mts +159 -0
- package/src/unblock-prs/checks.mts +538 -0
- package/src/unblock-prs/checks.test.mts +507 -0
- package/src/unblock-prs/claims.mts +33 -0
- package/src/unblock-prs/claims.test.mts +64 -0
- package/src/unblock-prs/config.mts +187 -0
- package/src/unblock-prs/config.schema.json +71 -0
- package/src/unblock-prs/config.test.mts +139 -0
- package/src/unblock-prs/constants.mts +33 -0
- package/src/unblock-prs/demotions.mts +73 -0
- package/src/unblock-prs/demotions.test.mts +100 -0
- package/src/unblock-prs/github.mts +1042 -0
- package/src/unblock-prs/github.test.mts +84 -0
- package/src/unblock-prs/index.mts +24 -0
- package/src/unblock-prs/labels.mts +70 -0
- package/src/unblock-prs/main.mts +1051 -0
- package/src/unblock-prs/merge-after.mts +117 -0
- package/src/unblock-prs/merge-after.test.mts +120 -0
- package/src/unblock-prs/options.mts +157 -0
- package/src/unblock-prs/owner.mts +91 -0
- package/src/unblock-prs/owner.test.mts +114 -0
- package/src/unblock-prs/quiet.mts +62 -0
- package/src/unblock-prs/quiet.test.mts +175 -0
- package/src/unblock-prs/rebase.mts +962 -0
- package/src/unblock-prs/release.mts +149 -0
- package/src/unblock-prs/release.test.mts +209 -0
- package/src/unblock-prs/set-aside-comment.mts +143 -0
- package/src/unblock-prs/set-aside-comment.test.mts +148 -0
- package/src/unblock-prs/set-aside-detail.mts +82 -0
- package/src/unblock-prs/set-aside-detail.test.mts +131 -0
- package/src/unblock-prs/skips.mts +215 -0
- package/src/unblock-prs/skips.test.mts +280 -0
- package/src/unblock-prs/stack.mts +220 -0
- package/src/unblock-prs/stack.test.mts +449 -0
- package/src/unblock-prs/triage.mts +669 -0
- package/src/unblock-prs/triage.test.mts +100 -0
- package/src/unblock-prs/types.mts +358 -0
- package/src/unblock-prs/util.mts +78 -0
- package/src/unblock-prs/util.test.mts +26 -0
- package/src/unblock-prs/version-pr.mts +112 -0
- package/src/unblock-prs/version-pr.test.mts +78 -0
- package/src/unblock-prs/watch.mts +219 -0
|
@@ -0,0 +1,962 @@
|
|
|
1
|
+
// cspell:ignore unlabel unlabelling retarget retargeted
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Moving a branch: onto the tip of the base, with the layers stacked on it
|
|
5
|
+
* carried along, and out from under `skip-ci` — or, for a layer the one below
|
|
6
|
+
* it merged from under, off that layer's commits and nothing more.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as os from 'node:os';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import {
|
|
12
|
+
isSafeRefName,
|
|
13
|
+
MERGE_QUEUED_LABEL,
|
|
14
|
+
SKIP_CI_LABEL,
|
|
15
|
+
stackDescendants,
|
|
16
|
+
type QueueOwner,
|
|
17
|
+
} from 'pr-report-core';
|
|
18
|
+
import { Arr, isRecord, Result } from 'ts-data-forge';
|
|
19
|
+
import { git, log, removeLabel, sh, type ApiContext } from '../lib/index.mjs';
|
|
20
|
+
import {
|
|
21
|
+
armAutoMerge,
|
|
22
|
+
mergedHeadOf,
|
|
23
|
+
readBaseChanges,
|
|
24
|
+
remoteSha,
|
|
25
|
+
removeWorktree,
|
|
26
|
+
viewPullRequest,
|
|
27
|
+
type MergedHead,
|
|
28
|
+
} from './github.mjs';
|
|
29
|
+
import { isMergeQueued, isSkipCiLabelled } from './labels.mjs';
|
|
30
|
+
import {
|
|
31
|
+
describeCommandFailure,
|
|
32
|
+
describeConflict,
|
|
33
|
+
} from './set-aside-detail.mjs';
|
|
34
|
+
import { withSkip } from './skips.mjs';
|
|
35
|
+
import {
|
|
36
|
+
autoRebaseTargets,
|
|
37
|
+
restackable,
|
|
38
|
+
retargetedFrom,
|
|
39
|
+
retargetedLayers,
|
|
40
|
+
stackParentsOf,
|
|
41
|
+
} from './stack.mjs';
|
|
42
|
+
import type {
|
|
43
|
+
Advanced,
|
|
44
|
+
AdvancePlan,
|
|
45
|
+
BranchOf,
|
|
46
|
+
PullRequest,
|
|
47
|
+
RebaseFailure,
|
|
48
|
+
RetargetedLayer,
|
|
49
|
+
SkipRecords,
|
|
50
|
+
StackedOn,
|
|
51
|
+
} from './types.mjs';
|
|
52
|
+
import { lastLines, stopRequested } from './util.mjs';
|
|
53
|
+
import { isVersionPullRequest } from './version-pr.mjs';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Where a branch is rebased to, and what it is rebased from.
|
|
57
|
+
*
|
|
58
|
+
* `onto` is a revision: `origin/<default branch>` for a pull request's own
|
|
59
|
+
* turn, or the new head of the layer below for a restack. `upstream`, when
|
|
60
|
+
* given, is the commit below the branch's own commits, and makes the rebase
|
|
61
|
+
* `git rebase --onto <onto> <upstream>`: exactly the commits after it are
|
|
62
|
+
* replayed, whatever their patches look like upstream. `required` says what
|
|
63
|
+
* to do when the branch does not contain it: fail, or fall back to a plain
|
|
64
|
+
* rebase, which drops commits already upstream by their patches.
|
|
65
|
+
*/
|
|
66
|
+
type RebaseTarget = Readonly<{
|
|
67
|
+
onto: string;
|
|
68
|
+
/** Refspecs to fetch from `origin` besides the branch itself. */
|
|
69
|
+
fetch: readonly string[];
|
|
70
|
+
upstream: Readonly<{ sha: string; required: boolean }> | undefined;
|
|
71
|
+
}>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Rebases the pull request's branch in a throwaway worktree and
|
|
75
|
+
* force-pushes the result, expecting the remote branch to still be at the
|
|
76
|
+
* head the survey saw. Resolves to the new head, or to `moved` when the
|
|
77
|
+
* branch is no longer where the survey saw it.
|
|
78
|
+
*
|
|
79
|
+
* The worktree is detached, so the checkout this runs from is never touched,
|
|
80
|
+
* and it is removed on the way out of every path: the work inside it is a
|
|
81
|
+
* function of its own, and this one removes the worktree after it returns
|
|
82
|
+
* however it returned. A process killed mid-rebase leaves one behind, which
|
|
83
|
+
* is what the `removeWorktree` before `git worktree add` is for.
|
|
84
|
+
*/
|
|
85
|
+
const rebaseAndPush = async (
|
|
86
|
+
pr: BranchOf,
|
|
87
|
+
target: RebaseTarget,
|
|
88
|
+
): Promise<Result<Rebased, RebaseFailure>> => {
|
|
89
|
+
const branch = pr.headRefName;
|
|
90
|
+
|
|
91
|
+
const worktreeDir = path.join(os.tmpdir(), 'unblock-prs', `pr-${pr.number}`);
|
|
92
|
+
|
|
93
|
+
const rebaseFailed = (detail: string): Result<Rebased, RebaseFailure> =>
|
|
94
|
+
Result.err({ reason: 'rebase-failed', detail });
|
|
95
|
+
|
|
96
|
+
// A previous run may have been interrupted with this worktree in place.
|
|
97
|
+
await removeWorktree(worktreeDir);
|
|
98
|
+
|
|
99
|
+
const fetched = await git(
|
|
100
|
+
Arr.toUnshifted('git fetch --quiet origin')(
|
|
101
|
+
Arr.toPushed(target.fetch, branch).map(sh),
|
|
102
|
+
).join(' '),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (Result.isErr(fetched)) {
|
|
106
|
+
return rebaseFailed(`fetch failed: ${fetched.value}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const remoteHead = await git(`git rev-parse ${sh(`origin/${branch}`)}`);
|
|
110
|
+
|
|
111
|
+
if (Result.isErr(remoteHead)) {
|
|
112
|
+
return rebaseFailed(`cannot resolve origin/${branch}: ${remoteHead.value}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (remoteHead.value.trim() !== pr.headRefOid) {
|
|
116
|
+
return Result.ok({ kind: 'moved' });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const added = await git(
|
|
120
|
+
`git worktree add --detach ${sh(worktreeDir)} ${sh(pr.headRefOid)}`,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (Result.isErr(added)) {
|
|
124
|
+
return rebaseFailed(`checkout failed: ${added.value}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const result = await rebaseInWorktree(pr, target, worktreeDir);
|
|
128
|
+
|
|
129
|
+
await removeWorktree(worktreeDir);
|
|
130
|
+
|
|
131
|
+
return result;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The part that happens inside the worktree, so that removing it afterwards
|
|
136
|
+
* is one statement in the caller rather than a `finally`. Every path here
|
|
137
|
+
* resolves to a `Result` — `git` turns a failing command into one — so the
|
|
138
|
+
* caller's next line always runs.
|
|
139
|
+
*/
|
|
140
|
+
const rebaseInWorktree = async (
|
|
141
|
+
pr: BranchOf,
|
|
142
|
+
target: RebaseTarget,
|
|
143
|
+
worktreeDir: string,
|
|
144
|
+
): Promise<Result<Rebased, RebaseFailure>> => {
|
|
145
|
+
const branch = pr.headRefName;
|
|
146
|
+
|
|
147
|
+
const rebaseFailed = (
|
|
148
|
+
detail: string,
|
|
149
|
+
output?: string,
|
|
150
|
+
): Result<Rebased, RebaseFailure> =>
|
|
151
|
+
Result.err({
|
|
152
|
+
reason: 'rebase-failed',
|
|
153
|
+
detail,
|
|
154
|
+
...(output === undefined ? {} : { output }),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const { upstream } = target;
|
|
158
|
+
|
|
159
|
+
const contained =
|
|
160
|
+
upstream !== undefined &&
|
|
161
|
+
Result.isOk(
|
|
162
|
+
await git(
|
|
163
|
+
`git merge-base --is-ancestor ${sh(upstream.sha)} HEAD`,
|
|
164
|
+
worktreeDir,
|
|
165
|
+
),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
if (!contained && upstream?.required === true) {
|
|
169
|
+
return rebaseFailed(
|
|
170
|
+
`it does not contain ${upstream.sha.slice(0, 10)}, the head it was stacked on, so which commits are its own cannot be told`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const rebased = await git(
|
|
175
|
+
contained
|
|
176
|
+
? `git rebase --onto ${sh(target.onto)} ${sh(upstream.sha)}`
|
|
177
|
+
: `git rebase ${sh(target.onto)}`,
|
|
178
|
+
worktreeDir,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (Result.isErr(rebased)) {
|
|
182
|
+
const conflicted = await git(
|
|
183
|
+
'git diff --name-only --diff-filter=U',
|
|
184
|
+
worktreeDir,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
await git('git rebase --abort', worktreeDir);
|
|
188
|
+
|
|
189
|
+
return rebaseFailed(
|
|
190
|
+
describeConflict(
|
|
191
|
+
Result.isOk(conflicted)
|
|
192
|
+
? conflicted.value.split('\n').filter((line) => line !== '')
|
|
193
|
+
: [],
|
|
194
|
+
lastLines(rebased.value, 5),
|
|
195
|
+
target.onto,
|
|
196
|
+
),
|
|
197
|
+
rebased.value,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const newHead = await git('git rev-parse HEAD', worktreeDir);
|
|
202
|
+
|
|
203
|
+
if (Result.isErr(newHead)) {
|
|
204
|
+
return rebaseFailed(`cannot read the rebased head: ${newHead.value}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const sha = newHead.value.trim();
|
|
208
|
+
|
|
209
|
+
if (sha === pr.headRefOid) {
|
|
210
|
+
return Result.ok({ kind: 'rebased', head: sha });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const ontoHead = await git(`git rev-parse ${sh(target.onto)}`, worktreeDir);
|
|
214
|
+
|
|
215
|
+
if (Result.isOk(ontoHead) && sha === ontoHead.value.trim()) {
|
|
216
|
+
// The rebase left the branch at what it was rebased onto: every commit
|
|
217
|
+
// on it had a patch already there, so `git rebase` skipped the lot.
|
|
218
|
+
// Pushing that would leave a pull request with no commits in it, which
|
|
219
|
+
// auto-merge will never merge, so leave the branch alone and say what
|
|
220
|
+
// happened.
|
|
221
|
+
return Result.err({
|
|
222
|
+
reason: 'already-in-base',
|
|
223
|
+
detail: `every commit is already in ${target.onto}; the pull request has nothing left to merge`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const pushed = await pushLeased(branch, sha, pr.headRefOid, worktreeDir);
|
|
228
|
+
|
|
229
|
+
if (Result.isErr(pushed)) {
|
|
230
|
+
return Result.err({
|
|
231
|
+
reason: 'push-failed',
|
|
232
|
+
detail: `push refused: ${lastLines(pushed.value, 5)}`,
|
|
233
|
+
output: pushed.value,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return Result.ok(
|
|
238
|
+
pushed.value === 'moved'
|
|
239
|
+
? { kind: 'moved' }
|
|
240
|
+
: { kind: 'rebased', head: sha },
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Force-pushes `sha` to `branch`, leased against `lease`: `moved` when the
|
|
246
|
+
* lease lost to someone else's push, an error when the push was refused for
|
|
247
|
+
* any other reason. The worker's rebases and restacks push through here, and
|
|
248
|
+
* so does `push-fixed-pr`, which a session runs.
|
|
249
|
+
*/
|
|
250
|
+
export const pushLeased = async (
|
|
251
|
+
branch: string,
|
|
252
|
+
sha: string,
|
|
253
|
+
lease: string,
|
|
254
|
+
cwd?: string,
|
|
255
|
+
): Promise<Result<'moved' | 'pushed', string>> => {
|
|
256
|
+
// The lease names the SHA the survey saw, not the remote-tracking ref: a
|
|
257
|
+
// bare `--force-with-lease` compares against whatever the last fetch
|
|
258
|
+
// left, which is exactly the race this is meant to lose safely.
|
|
259
|
+
const pushed = await git(
|
|
260
|
+
`git push --force-with-lease=${sh(`${branch}:${lease}`)} origin ${sh(`${sha}:refs/heads/${branch}`)}`,
|
|
261
|
+
cwd,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
if (Result.isOk(pushed)) {
|
|
265
|
+
return Result.ok('pushed');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// A lease that lost is someone else pushing in the time the rebase took,
|
|
269
|
+
// and that is not a reason to set the pull request aside.
|
|
270
|
+
const remoteHeadNow = await remoteSha(branch);
|
|
271
|
+
|
|
272
|
+
return Result.isOk(remoteHeadNow) && remoteHeadNow.value !== lease
|
|
273
|
+
? Result.ok('moved')
|
|
274
|
+
: Result.err(pushed.value);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
type Rebased = Readonly<{ kind: 'moved' } | { kind: 'rebased'; head: string }>;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Brings one pull request as close to merging as this script can: onto the
|
|
281
|
+
* tip of the base, with the layers stacked on it carried along, armed, and
|
|
282
|
+
* out from under `skip-ci`.
|
|
283
|
+
*
|
|
284
|
+
* The order is the point. While `skip-ci` is on, the push the rebase makes
|
|
285
|
+
* fires a `synchronize` whose every check workflow skips, so it costs
|
|
286
|
+
* nothing; removing the label afterwards fires `unlabeled`, and the matrix
|
|
287
|
+
* that starts then runs once, on the head that will actually be merged. Doing
|
|
288
|
+
* it the other way round starts a full matrix on the pre-rebase head and has
|
|
289
|
+
* the push cancel it through the concurrency group. Arming comes between
|
|
290
|
+
* the two, so that while it happens the label still holds the merge.
|
|
291
|
+
*
|
|
292
|
+
* A pull request GitHub moved off a stack is rebased with `--onto` from the
|
|
293
|
+
* head its merged parent was merged at, when it still contains that head —
|
|
294
|
+
* GitHub's own rebase on retargeting can fail, and then the branch still
|
|
295
|
+
* carries the parent's commits, whose patches match the squash commit only
|
|
296
|
+
* if nothing changed them on the way in. Otherwise the plain rebase drops
|
|
297
|
+
* them by patch, as it always has.
|
|
298
|
+
*
|
|
299
|
+
* A rebase that changes nothing is a failure only when there was no label to
|
|
300
|
+
* take off either: then GitHub's `BEHIND` or `DIRTY` was stale and the survey
|
|
301
|
+
* has to start over. A paused pull request that is already on top of the base
|
|
302
|
+
* is simply one whose only obstacle was the label.
|
|
303
|
+
*
|
|
304
|
+
* A branch that moves while this is moving it is someone else moving the same
|
|
305
|
+
* pull request — the skill runs beside this script, and a person may push —
|
|
306
|
+
* so it resolves to `moved` and leaves the rest to that someone. Recording it
|
|
307
|
+
* as a failure would set a pull request aside for being worked on, and trying
|
|
308
|
+
* the next candidate would start a second matrix beside the one the other
|
|
309
|
+
* writer is about to start.
|
|
310
|
+
*
|
|
311
|
+
* The version pull request is the exception: only the label comes off it.
|
|
312
|
+
* `changesets/action` rebuilds that branch from the tip of the base itself
|
|
313
|
+
* and force-pushes the result, so rebasing it here would be a second thing
|
|
314
|
+
* force-pushing one branch — and would carry the old version commit onto a
|
|
315
|
+
* tip whose changesets it never consumed. Triage has already established
|
|
316
|
+
* that the branch is on the tip; there is nothing left to move.
|
|
317
|
+
*/
|
|
318
|
+
export const advance = async (
|
|
319
|
+
api: ApiContext,
|
|
320
|
+
pr: PullRequest,
|
|
321
|
+
defaultBranch: string,
|
|
322
|
+
plan: AdvancePlan,
|
|
323
|
+
): Promise<Result<Advanced, RebaseFailure>> => {
|
|
324
|
+
if (isVersionPullRequest(pr, defaultBranch)) {
|
|
325
|
+
if (!isSkipCiLabelled(pr)) {
|
|
326
|
+
// Triage only ever offers this one while it is paused, so nothing to
|
|
327
|
+
// take off means the survey is a release behind.
|
|
328
|
+
return Result.ok({ kind: 'stale-merge-state' });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (plan.arm) {
|
|
332
|
+
const armed = await armOnPick(api, pr, pr.headRefOid, defaultBranch);
|
|
333
|
+
|
|
334
|
+
if (Result.isErr(armed) || armed.value === 'moved') {
|
|
335
|
+
return Result.isErr(armed) ? armed : Result.ok({ kind: 'moved' });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const unlabelled = await removeSkipCiLabel(api, pr, pr.headRefOid);
|
|
340
|
+
|
|
341
|
+
return Result.isErr(unlabelled)
|
|
342
|
+
? unlabelled
|
|
343
|
+
: Result.ok(
|
|
344
|
+
unlabelled.value === 'moved'
|
|
345
|
+
? { kind: 'moved' }
|
|
346
|
+
: { kind: 'advanced', head: pr.headRefOid },
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const merged = await stackedOnMerged(api, pr, defaultBranch);
|
|
351
|
+
|
|
352
|
+
const rebased = await rebaseAndPush(pr, {
|
|
353
|
+
onto: `origin/${defaultBranch}`,
|
|
354
|
+
fetch:
|
|
355
|
+
merged === undefined
|
|
356
|
+
? [defaultBranch]
|
|
357
|
+
: [defaultBranch, `refs/pull/${merged.number}/head`],
|
|
358
|
+
upstream:
|
|
359
|
+
merged === undefined
|
|
360
|
+
? undefined
|
|
361
|
+
: { sha: merged.headSha, required: false },
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
if (Result.isErr(rebased)) {
|
|
365
|
+
return rebased;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (rebased.value.kind === 'moved') {
|
|
369
|
+
return Result.ok({ kind: 'moved' });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const { head } = rebased.value;
|
|
373
|
+
|
|
374
|
+
if (head !== pr.headRefOid) {
|
|
375
|
+
await restack(pr, head, plan.descendants);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (plan.arm) {
|
|
379
|
+
const armed = await armOnPick(api, pr, head, defaultBranch);
|
|
380
|
+
|
|
381
|
+
if (Result.isErr(armed) || armed.value === 'moved') {
|
|
382
|
+
return Result.isErr(armed) ? armed : Result.ok({ kind: 'moved' });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (!isSkipCiLabelled(pr)) {
|
|
387
|
+
return head === pr.headRefOid
|
|
388
|
+
? Result.ok({ kind: 'stale-merge-state' })
|
|
389
|
+
: Result.ok({ kind: 'advanced', head });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const removed = await removeSkipCiLabel(api, pr, head);
|
|
393
|
+
|
|
394
|
+
return Result.isErr(removed)
|
|
395
|
+
? removed
|
|
396
|
+
: Result.ok(
|
|
397
|
+
removed.value === 'moved'
|
|
398
|
+
? { kind: 'moved' }
|
|
399
|
+
: { kind: 'advanced', head },
|
|
400
|
+
);
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Rebases every pull request GitHub moved onto the default branch when the
|
|
405
|
+
* layer below it merged, and that still carries that layer's commits, off
|
|
406
|
+
* them: `--onto` from the head the layer merged at to the squash commit it
|
|
407
|
+
* merged as, carrying the layers stacked on it along. `stack.mts` says why
|
|
408
|
+
* this waits for no queue, and why the tree comes out as it went in.
|
|
409
|
+
*
|
|
410
|
+
* Nothing is armed or released: `skip-ci` stays as it is, and a push under
|
|
411
|
+
* it fires a `synchronize` whose every check skips. Resolves to whether any
|
|
412
|
+
* branch moved — by this, or by someone else under it — since either makes
|
|
413
|
+
* the heads the survey read stale.
|
|
414
|
+
*/
|
|
415
|
+
export const restackRetargeted = async (
|
|
416
|
+
api: ApiContext,
|
|
417
|
+
before: StackedOn | undefined,
|
|
418
|
+
pullRequests: readonly PullRequest[],
|
|
419
|
+
defaultBranch: string,
|
|
420
|
+
dryRun: boolean,
|
|
421
|
+
): Promise<boolean> => {
|
|
422
|
+
const stackParents = stackParentsOf(pullRequests, defaultBranch);
|
|
423
|
+
|
|
424
|
+
let mut_moved = false;
|
|
425
|
+
|
|
426
|
+
for (const layer of retargetedLayers(before, pullRequests, defaultBranch)) {
|
|
427
|
+
if (stopRequested()) {
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const outcome = await restackOffMerged(api, layer, defaultBranch, {
|
|
432
|
+
dryRun,
|
|
433
|
+
descendants: stackDescendants(stackParents, layer.pr.number).flatMap(
|
|
434
|
+
(number) => pullRequests.filter((pr) => pr.number === number),
|
|
435
|
+
),
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
mut_moved ||= outcome !== 'untouched';
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return mut_moved;
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Rebases the pull requests that asked for it and are paused by `skip-ci`
|
|
446
|
+
* and behind the default branch onto it, carrying the layers stacked on each
|
|
447
|
+
* along, so that a stack waiting for review or for its turn keeps a diff of
|
|
448
|
+
* its own changes only. The push runs no checks: the label skips every workflow.
|
|
449
|
+
* `stack.mts` says which pull requests are eligible.
|
|
450
|
+
*
|
|
451
|
+
* A rebase that conflicts, or a push that is refused, is a set-aside like any
|
|
452
|
+
* other and is returned as one, tied to the head and the base it was tried at.
|
|
453
|
+
* Resolves to those records and whether any branch moved — by this, or by
|
|
454
|
+
* someone else under it — since either makes the survey stale.
|
|
455
|
+
*/
|
|
456
|
+
export const restackBehind = async (
|
|
457
|
+
pullRequests: readonly PullRequest[],
|
|
458
|
+
defaultBranch: string,
|
|
459
|
+
context: Readonly<{
|
|
460
|
+
baseSha: string;
|
|
461
|
+
skipped: SkipRecords;
|
|
462
|
+
owners: ReadonlyMap<number, QueueOwner>;
|
|
463
|
+
dryRun: boolean;
|
|
464
|
+
}>,
|
|
465
|
+
): Promise<Readonly<{ skipped: SkipRecords; moved: boolean }>> => {
|
|
466
|
+
const targets = autoRebaseTargets(pullRequests, defaultBranch, context);
|
|
467
|
+
|
|
468
|
+
if (!Arr.isNonEmpty(targets)) {
|
|
469
|
+
return { skipped: context.skipped, moved: false };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const fetched = await git(
|
|
473
|
+
Arr.toUnshifted('git fetch --quiet origin')(
|
|
474
|
+
Arr.toUnshifted(defaultBranch)(targets.map((pr) => pr.headRefName)).map(
|
|
475
|
+
sh,
|
|
476
|
+
),
|
|
477
|
+
).join(' '),
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
if (Result.isErr(fetched)) {
|
|
481
|
+
log(`Cannot fetch to see what is behind: ${lastLines(fetched.value, 2)}`);
|
|
482
|
+
|
|
483
|
+
return { skipped: context.skipped, moved: false };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const stackParents = stackParentsOf(pullRequests, defaultBranch);
|
|
487
|
+
|
|
488
|
+
let mut_skipped = context.skipped;
|
|
489
|
+
|
|
490
|
+
let mut_moved = false;
|
|
491
|
+
|
|
492
|
+
for (const pr of targets) {
|
|
493
|
+
if (stopRequested()) {
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Ancestry rather than `mergeStateStatus`: `skip-ci` holds that at
|
|
498
|
+
// `BLOCKED` whether or not the branch is behind.
|
|
499
|
+
const upToDate = await git(
|
|
500
|
+
`git merge-base --is-ancestor ${sh(`origin/${defaultBranch}`)} ${sh(pr.headRefOid)}`,
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
if (Result.isOk(upToDate)) {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (context.dryRun) {
|
|
508
|
+
log(`Would rebase #${pr.number} onto ${defaultBranch}; it is behind.`);
|
|
509
|
+
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const rebased = await rebaseAndPush(pr, {
|
|
514
|
+
onto: `origin/${defaultBranch}`,
|
|
515
|
+
fetch: [defaultBranch],
|
|
516
|
+
upstream: undefined,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
if (Result.isErr(rebased)) {
|
|
520
|
+
log(
|
|
521
|
+
`#${pr.number}: behind ${defaultBranch}, and not rebased — ${rebased.value.detail}`,
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
mut_skipped = withSkip(mut_skipped, {
|
|
525
|
+
number: pr.number,
|
|
526
|
+
headSha: pr.headRefOid,
|
|
527
|
+
baseSha: context.baseSha,
|
|
528
|
+
reason: rebased.value.reason,
|
|
529
|
+
detail: rebased.value.detail,
|
|
530
|
+
...(rebased.value.output === undefined
|
|
531
|
+
? {}
|
|
532
|
+
: { output: rebased.value.output }),
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (rebased.value.kind === 'moved') {
|
|
539
|
+
mut_moved = true;
|
|
540
|
+
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const { head } = rebased.value;
|
|
545
|
+
|
|
546
|
+
if (head === pr.headRefOid) {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
mut_moved = true;
|
|
551
|
+
|
|
552
|
+
log(
|
|
553
|
+
`#${pr.number}: was behind ${defaultBranch}; rebased at ${head.slice(0, 10)}.`,
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
await restack(
|
|
557
|
+
pr,
|
|
558
|
+
head,
|
|
559
|
+
stackDescendants(stackParents, pr.number).flatMap((number) =>
|
|
560
|
+
pullRequests.filter((layer) => layer.number === number),
|
|
561
|
+
),
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return { skipped: mut_skipped, moved: mut_moved };
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Takes `skip-ci` off, having first asked GitHub whether the pull request
|
|
570
|
+
* still wants it. Removing the label starts a CI matrix, and the survey it
|
|
571
|
+
* was decided on is a rebase old by now.
|
|
572
|
+
*/
|
|
573
|
+
const removeSkipCiLabel = async (
|
|
574
|
+
api: ApiContext,
|
|
575
|
+
pr: PullRequest,
|
|
576
|
+
head: string,
|
|
577
|
+
): Promise<Result<'moved' | 'removed', RebaseFailure>> => {
|
|
578
|
+
const failed = (
|
|
579
|
+
detail: string,
|
|
580
|
+
output?: string,
|
|
581
|
+
): Result<'moved' | 'removed', RebaseFailure> =>
|
|
582
|
+
Result.err({
|
|
583
|
+
reason: 'unlabel-failed',
|
|
584
|
+
detail,
|
|
585
|
+
...(output === undefined ? {} : { output }),
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
const current = await viewPullRequest(api, pr.number);
|
|
589
|
+
|
|
590
|
+
if (Result.isErr(current)) {
|
|
591
|
+
return failed(`cannot re-read it before unlabelling: ${current.value}`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (current.value.state !== 'OPEN') {
|
|
595
|
+
return failed(`it is ${current.value.state} now`);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (current.value.headRefOid !== head) {
|
|
599
|
+
return Result.ok('moved');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (!isSkipCiLabelled(current.value)) {
|
|
603
|
+
// Someone took the label off in the meantime, which is the thing this was
|
|
604
|
+
// about to do.
|
|
605
|
+
return Result.ok('removed');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (!isMergeQueued(current.value)) {
|
|
609
|
+
return failed(`${MERGE_QUEUED_LABEL} came off while it was rebasing`);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const removed = await removeLabel(api, pr.number, SKIP_CI_LABEL);
|
|
613
|
+
|
|
614
|
+
return Result.isErr(removed)
|
|
615
|
+
? failed(
|
|
616
|
+
`cannot remove ${SKIP_CI_LABEL}: ${describeCommandFailure(removed.value)}`,
|
|
617
|
+
removed.value,
|
|
618
|
+
)
|
|
619
|
+
: Result.ok('removed');
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* The layer this pull request was stacked on, if GitHub moved it onto the
|
|
624
|
+
* default branch when that layer merged — and so the head below its own
|
|
625
|
+
* commits, if GitHub did not rebase it. Anything that cannot be read is
|
|
626
|
+
* `undefined`, and the rebase falls back to dropping commits by their
|
|
627
|
+
* patches, which a one-commit layer squash-merged unchanged also survives.
|
|
628
|
+
*/
|
|
629
|
+
const stackedOnMerged = async (
|
|
630
|
+
api: ApiContext,
|
|
631
|
+
pr: PullRequest,
|
|
632
|
+
defaultBranch: string,
|
|
633
|
+
): Promise<MergedHead | undefined> => {
|
|
634
|
+
const changes = await readBaseChanges(api, pr.number);
|
|
635
|
+
|
|
636
|
+
if (Result.isErr(changes)) {
|
|
637
|
+
log(
|
|
638
|
+
`#${pr.number}: cannot read whether it came off a stack; rebasing by patch. (${lastLines(changes.value, 2)})`,
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const from = retargetedFrom(changes.value, defaultBranch);
|
|
645
|
+
|
|
646
|
+
if (from === undefined || !isSafeRefName(from)) {
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const merged = await mergedHeadOf(api, from);
|
|
651
|
+
|
|
652
|
+
if (Result.isErr(merged)) {
|
|
653
|
+
log(
|
|
654
|
+
`#${pr.number}: cannot read what merged from ${from}; rebasing by patch. (${lastLines(merged.value, 2)})`,
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
return undefined;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return merged.value;
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* One layer of `restackRetargeted`. Whatever cannot be read or done is said
|
|
665
|
+
* and left: the layer is where GitHub put it, which is where it would be
|
|
666
|
+
* without this.
|
|
667
|
+
*/
|
|
668
|
+
const restackOffMerged = async (
|
|
669
|
+
api: ApiContext,
|
|
670
|
+
{ pr, from }: RetargetedLayer,
|
|
671
|
+
defaultBranch: string,
|
|
672
|
+
plan: Readonly<{ dryRun: boolean; descendants: readonly PullRequest[] }>,
|
|
673
|
+
): Promise<'moved' | 'restacked' | 'untouched'> => {
|
|
674
|
+
const branch = from ?? (await retargetedFromTimeline(api, pr, defaultBranch));
|
|
675
|
+
|
|
676
|
+
if (branch === undefined || !isSafeRefName(branch)) {
|
|
677
|
+
return 'untouched';
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const merged = await mergedHeadOf(api, branch);
|
|
681
|
+
|
|
682
|
+
if (Result.isErr(merged)) {
|
|
683
|
+
log(
|
|
684
|
+
`#${pr.number}: cannot read what merged from ${branch}, so not rebasing it off that. (${lastLines(merged.value, 2)})`,
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
return 'untouched';
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Nothing merged from that branch: the layer below was closed, or a person
|
|
691
|
+
// moved this one.
|
|
692
|
+
if (merged.value === undefined) {
|
|
693
|
+
return 'untouched';
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const { number, headSha, mergeCommit } = merged.value;
|
|
697
|
+
|
|
698
|
+
const carried = await carries(pr, headSha, [
|
|
699
|
+
defaultBranch,
|
|
700
|
+
`refs/pull/${number}/head`,
|
|
701
|
+
]);
|
|
702
|
+
|
|
703
|
+
if (Result.isErr(carried)) {
|
|
704
|
+
log(
|
|
705
|
+
`#${pr.number}: cannot tell whether it still carries #${number}'s commits. (${lastLines(carried.value, 2)})`,
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
return 'untouched';
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// GitHub rebased it itself, or this already has.
|
|
712
|
+
if (!carried.value) {
|
|
713
|
+
return 'untouched';
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (mergeCommit === undefined) {
|
|
717
|
+
log(
|
|
718
|
+
`#${pr.number}: still carries #${number}'s commits, but GitHub does not say what #${number} merged as; leaving it.`,
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
return 'untouched';
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (plan.dryRun) {
|
|
725
|
+
log(
|
|
726
|
+
`Would rebase #${pr.number} off #${number}'s commits, onto the squash commit ${mergeCommit.slice(0, 10)}.`,
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
return 'untouched';
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// `carries` has just fetched everything the rebase needs.
|
|
733
|
+
const rebased = await rebaseAndPush(pr, {
|
|
734
|
+
onto: mergeCommit,
|
|
735
|
+
fetch: [],
|
|
736
|
+
upstream: { sha: headSha, required: true },
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
if (Result.isErr(rebased)) {
|
|
740
|
+
log(
|
|
741
|
+
`#${pr.number}: still carries #${number}'s commits, and cannot be rebased off them — ${rebased.value.detail}`,
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
return 'untouched';
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (rebased.value.kind === 'moved') {
|
|
748
|
+
log(
|
|
749
|
+
`#${pr.number}: not rebased off #${number}'s commits — its branch moved since the survey, so someone else is moving it.`,
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
return 'moved';
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const { head } = rebased.value;
|
|
756
|
+
|
|
757
|
+
log(
|
|
758
|
+
`#${pr.number}: came off #${number} still carrying its commits; rebased its own onto #${number}'s squash commit ${mergeCommit.slice(0, 10)}, at ${head.slice(0, 10)}.`,
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
await restack(pr, head, plan.descendants);
|
|
762
|
+
|
|
763
|
+
return 'restacked';
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* The branch the pull request's timeline says GitHub moved it off, if it
|
|
768
|
+
* did. A timeline that cannot be read says nothing.
|
|
769
|
+
*/
|
|
770
|
+
const retargetedFromTimeline = async (
|
|
771
|
+
api: ApiContext,
|
|
772
|
+
pr: PullRequest,
|
|
773
|
+
defaultBranch: string,
|
|
774
|
+
): Promise<string | undefined> => {
|
|
775
|
+
const changes = await readBaseChanges(api, pr.number);
|
|
776
|
+
|
|
777
|
+
if (Result.isErr(changes)) {
|
|
778
|
+
log(
|
|
779
|
+
`#${pr.number}: cannot read whether it came off a stack, so not rebasing it off one. (${lastLines(changes.value, 2)})`,
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
return undefined;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return retargetedFrom(changes.value, defaultBranch);
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Whether the pull request's head, as the survey saw it, contains `sha`,
|
|
790
|
+
* having fetched the branch and `refspecs` to find out.
|
|
791
|
+
*/
|
|
792
|
+
const carries = async (
|
|
793
|
+
pr: PullRequest,
|
|
794
|
+
sha: string,
|
|
795
|
+
refspecs: readonly string[],
|
|
796
|
+
): Promise<Result<boolean, string>> => {
|
|
797
|
+
const fetched = await git(
|
|
798
|
+
Arr.toUnshifted('git fetch --quiet origin')(
|
|
799
|
+
Arr.toPushed(refspecs, pr.headRefName).map(sh),
|
|
800
|
+
).join(' '),
|
|
801
|
+
);
|
|
802
|
+
|
|
803
|
+
if (Result.isErr(fetched)) {
|
|
804
|
+
return Result.err(fetched.value);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const contained = await git(
|
|
808
|
+
`git merge-base --is-ancestor ${sh(sha)} ${sh(pr.headRefOid)}`,
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
return Result.ok(Result.isOk(contained));
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Carries every layer stacked on `pr` along after it moved from its old head
|
|
816
|
+
* to `head`: each is replayed with `--onto` from the head it was on to the
|
|
817
|
+
* one that replaced it, lowest layer first so that each finds its own new
|
|
818
|
+
* base already pushed. A layer that cannot be moved, or that someone else
|
|
819
|
+
* moved first, is reported and left where it is, and so is everything on
|
|
820
|
+
* it; none of this stops `pr` itself, whose turn it is. `push-fixed-pr`
|
|
821
|
+
* carries a session's layers along through here too.
|
|
822
|
+
*/
|
|
823
|
+
export const restack = async (
|
|
824
|
+
pr: BranchOf,
|
|
825
|
+
head: string,
|
|
826
|
+
descendants: readonly BranchOf[],
|
|
827
|
+
): Promise<void> => {
|
|
828
|
+
const mut_moved = new Map<string, Readonly<{ from: string; to: string }>>([
|
|
829
|
+
[pr.headRefName, { from: pr.headRefOid, to: head }],
|
|
830
|
+
]);
|
|
831
|
+
|
|
832
|
+
for (const layer of descendants) {
|
|
833
|
+
const below = mut_moved.get(layer.baseRefName);
|
|
834
|
+
|
|
835
|
+
if (below === undefined) {
|
|
836
|
+
log(
|
|
837
|
+
`#${layer.number}: not restacked — the layer it is on, ${layer.baseRefName}, did not move.`,
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const refused = restackable(layer);
|
|
844
|
+
|
|
845
|
+
if (refused !== undefined) {
|
|
846
|
+
log(`#${layer.number}: not restacked — ${refused}.`);
|
|
847
|
+
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const moved = await rebaseAndPush(layer, {
|
|
852
|
+
onto: below.to,
|
|
853
|
+
fetch: [],
|
|
854
|
+
upstream: { sha: below.from, required: true },
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
if (Result.isErr(moved)) {
|
|
858
|
+
log(
|
|
859
|
+
`#${layer.number}: not restacked onto ${layer.baseRefName} — ${moved.value.detail}`,
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (moved.value.kind === 'moved') {
|
|
866
|
+
log(
|
|
867
|
+
`#${layer.number}: not restacked — its branch moved since the survey, so someone else is moving it.`,
|
|
868
|
+
);
|
|
869
|
+
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
log(
|
|
874
|
+
`#${layer.number}: restacked onto ${layer.baseRefName} at ${below.to.slice(0, 10)}.`,
|
|
875
|
+
);
|
|
876
|
+
|
|
877
|
+
mut_moved.set(layer.headRefName, {
|
|
878
|
+
from: layer.headRefOid,
|
|
879
|
+
to: moved.value.head,
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Arms auto-merge on the pull request this cycle picked, having first asked
|
|
886
|
+
* whether it is still what triage saw: open, on the default branch, queued,
|
|
887
|
+
* at `head`. A head other than that is someone else moving it, as in
|
|
888
|
+
* `removeSkipCiLabel`.
|
|
889
|
+
*
|
|
890
|
+
* Called for one it rebases after the push and before `skip-ci` comes off, so
|
|
891
|
+
* that the label holds the merge until the matrix that decides it starts; and
|
|
892
|
+
* for one already up to date before it is watched.
|
|
893
|
+
*
|
|
894
|
+
* Nothing else arms one. `open-pr` opens a pull request with none, and the
|
|
895
|
+
* author queueing it with `merge-queued` is the request to land it. Arming at
|
|
896
|
+
* pick time rather than at opening is what keeps a stacked pull request
|
|
897
|
+
* unarmed while it is stacked: its base is another pull request's branch,
|
|
898
|
+
* which no ruleset covers, so armed there it would merge into that branch
|
|
899
|
+
* the moment nothing held it. It is picked only once GitHub has moved it onto
|
|
900
|
+
* the default branch, where the ruleset gates the merge.
|
|
901
|
+
*
|
|
902
|
+
* `merge-queued` is the only thing read as permission, so one that has lost
|
|
903
|
+
* its auto-merge — GitHub disarms it when the base changes, and a person may
|
|
904
|
+
* switch it off — is armed again when it is next picked. Holding one back is
|
|
905
|
+
* taking `merge-queued` off.
|
|
906
|
+
*/
|
|
907
|
+
export const armOnPick = async (
|
|
908
|
+
api: ApiContext,
|
|
909
|
+
pr: PullRequest,
|
|
910
|
+
head: string,
|
|
911
|
+
defaultBranch: string,
|
|
912
|
+
): Promise<Result<'armed' | 'moved', RebaseFailure>> => {
|
|
913
|
+
const failed = (
|
|
914
|
+
detail: string,
|
|
915
|
+
output?: string,
|
|
916
|
+
): Result<'armed' | 'moved', RebaseFailure> =>
|
|
917
|
+
Result.err({
|
|
918
|
+
reason: 'arm-failed',
|
|
919
|
+
detail,
|
|
920
|
+
...(output === undefined ? {} : { output }),
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
const current = await viewPullRequest(api, pr.number);
|
|
924
|
+
|
|
925
|
+
if (Result.isErr(current)) {
|
|
926
|
+
return failed(`cannot re-read it before arming: ${current.value}`);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (current.value.state === 'OPEN' && current.value.headRefOid !== head) {
|
|
930
|
+
return Result.ok('moved');
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const refused =
|
|
934
|
+
current.value.state !== 'OPEN'
|
|
935
|
+
? (`it is ${current.value.state} now` as const)
|
|
936
|
+
: current.value.baseRefName !== defaultBranch
|
|
937
|
+
? (`its base is ${current.value.baseRefName} now` as const)
|
|
938
|
+
: !isMergeQueued(current.value)
|
|
939
|
+
? (`${MERGE_QUEUED_LABEL} came off before it was armed` as const)
|
|
940
|
+
: undefined;
|
|
941
|
+
|
|
942
|
+
if (refused !== undefined) {
|
|
943
|
+
return failed(`not arming auto-merge: ${refused}`);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (isRecord(current.value.autoMergeRequest)) {
|
|
947
|
+
return Result.ok('armed');
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const armed = await armAutoMerge(api, current.value.id);
|
|
951
|
+
|
|
952
|
+
if (Result.isErr(armed)) {
|
|
953
|
+
return failed(
|
|
954
|
+
`cannot arm auto-merge: ${describeCommandFailure(armed.value)}`,
|
|
955
|
+
armed.value,
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
log(`#${pr.number}: auto-merge armed.`);
|
|
960
|
+
|
|
961
|
+
return Result.ok('armed');
|
|
962
|
+
};
|