bstack 1.1.6 → 1.2.1
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/README.md +5 -0
- package/dist/bstack.mjs +105 -63
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,6 +73,11 @@ git rebase --continue
|
|
|
73
73
|
bstack
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
### Reorder pull requests
|
|
77
|
+
|
|
78
|
+
Reorder the corresponding commits with Git, then run `bstack` again.
|
|
79
|
+
`bstack` keeps the existing pull request identities and rebuilds the native GitHub stack in the new commit order.
|
|
80
|
+
|
|
76
81
|
Stacks cannot contain merge commits.
|
|
77
82
|
When `main` moves, rebase your branch onto it instead of merging `main` into your branch.
|
|
78
83
|
|
package/dist/bstack.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import * as v from "valibot";
|
|
|
6
6
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { dirname } from "node:path";
|
|
8
8
|
//#region package.json
|
|
9
|
-
var version = "1.1
|
|
9
|
+
var version = "1.2.1";
|
|
10
10
|
//#endregion
|
|
11
11
|
//#region src/command.ts
|
|
12
12
|
var CommandError = class extends Error {
|
|
@@ -59,7 +59,7 @@ function checkoutStack(dependencies, options) {
|
|
|
59
59
|
const { repository, github, reporter } = dependencies;
|
|
60
60
|
reporter.progress("Checking the repository and GitHub prerequisites");
|
|
61
61
|
repository.assertReady();
|
|
62
|
-
repository.
|
|
62
|
+
if (!repository.isClean()) throw new Error("The working tree must be clean before checkout");
|
|
63
63
|
github.assertReady();
|
|
64
64
|
const remote = repository.resolveRemote(options.remote);
|
|
65
65
|
reporter.progress(`Looking up pull request ${options.reference}`);
|
|
@@ -168,8 +168,8 @@ var GitCliRepository = class {
|
|
|
168
168
|
assertReady() {
|
|
169
169
|
this.git(["rev-parse", "--show-toplevel"]);
|
|
170
170
|
}
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
isClean() {
|
|
172
|
+
return this.git(["status", "--porcelain"]).stdout.trim() === "";
|
|
173
173
|
}
|
|
174
174
|
currentBranch() {
|
|
175
175
|
return this.git([
|
|
@@ -177,7 +177,7 @@ var GitCliRepository = class {
|
|
|
177
177
|
"--quiet",
|
|
178
178
|
"--short",
|
|
179
179
|
"HEAD"
|
|
180
|
-
], { allowFailure: true }).stdout.trim();
|
|
180
|
+
], { allowFailure: true }).stdout.trim() || void 0;
|
|
181
181
|
}
|
|
182
182
|
resolveRemote(requested) {
|
|
183
183
|
if (requested) {
|
|
@@ -327,6 +327,14 @@ const pullRequestSchema = v.object({
|
|
|
327
327
|
body: v.string(),
|
|
328
328
|
isDraft: v.boolean()
|
|
329
329
|
});
|
|
330
|
+
const createdPullRequestSchema = v.object({
|
|
331
|
+
number: v.number(),
|
|
332
|
+
html_url: v.string(),
|
|
333
|
+
state: v.picklist(["open", "closed"]),
|
|
334
|
+
title: v.string(),
|
|
335
|
+
body: v.nullable(v.string()),
|
|
336
|
+
draft: v.boolean()
|
|
337
|
+
});
|
|
330
338
|
const stackSchema = v.object({ number: v.number() });
|
|
331
339
|
var GitHubCliPlatform = class {
|
|
332
340
|
cwd;
|
|
@@ -392,25 +400,33 @@ var GitHubCliPlatform = class {
|
|
|
392
400
|
return v.parse(pullRequestSchema, JSON.parse(raw));
|
|
393
401
|
}
|
|
394
402
|
createPullRequest(change, base, draft) {
|
|
395
|
-
const
|
|
396
|
-
"
|
|
397
|
-
"
|
|
398
|
-
"
|
|
399
|
-
|
|
400
|
-
"--
|
|
401
|
-
|
|
402
|
-
"--
|
|
403
|
-
change.
|
|
404
|
-
"--
|
|
405
|
-
change.
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
403
|
+
const raw = this.gh([
|
|
404
|
+
"api",
|
|
405
|
+
"--method",
|
|
406
|
+
"POST",
|
|
407
|
+
"repos/{owner}/{repo}/pulls",
|
|
408
|
+
"--raw-field",
|
|
409
|
+
`base=${base}`,
|
|
410
|
+
"--raw-field",
|
|
411
|
+
`head=${change.remoteBranch}`,
|
|
412
|
+
"--raw-field",
|
|
413
|
+
`title=${change.subject}`,
|
|
414
|
+
"--raw-field",
|
|
415
|
+
`body=${change.body}`,
|
|
416
|
+
"--field",
|
|
417
|
+
`draft=${draft}`
|
|
418
|
+
]).stdout;
|
|
419
|
+
const created = v.parse(createdPullRequestSchema, JSON.parse(raw));
|
|
420
|
+
return {
|
|
421
|
+
number: created.number,
|
|
422
|
+
url: created.html_url,
|
|
423
|
+
state: created.state === "open" ? "OPEN" : "CLOSED",
|
|
424
|
+
title: created.title,
|
|
425
|
+
body: created.body ?? "",
|
|
426
|
+
isDraft: created.draft
|
|
427
|
+
};
|
|
412
428
|
}
|
|
413
|
-
linkStack(
|
|
429
|
+
linkStack(pullRequests, base, remote, draft) {
|
|
414
430
|
const args = [
|
|
415
431
|
"stack",
|
|
416
432
|
"link",
|
|
@@ -420,10 +436,10 @@ var GitHubCliPlatform = class {
|
|
|
420
436
|
remote
|
|
421
437
|
];
|
|
422
438
|
if (!draft) args.push("--open");
|
|
423
|
-
args.push(...
|
|
439
|
+
args.push(...pullRequests.map(String));
|
|
424
440
|
this.gh(args);
|
|
425
441
|
}
|
|
426
|
-
appendToStack(stackNumber,
|
|
442
|
+
appendToStack(stackNumber, pullRequests, remote, draft) {
|
|
427
443
|
const args = [
|
|
428
444
|
"stack",
|
|
429
445
|
"link",
|
|
@@ -431,7 +447,7 @@ var GitHubCliPlatform = class {
|
|
|
431
447
|
remote
|
|
432
448
|
];
|
|
433
449
|
if (!draft) args.push("--open");
|
|
434
|
-
args.push(String(stackNumber), ...
|
|
450
|
+
args.push(String(stackNumber), ...pullRequests.map(String));
|
|
435
451
|
this.gh(args);
|
|
436
452
|
}
|
|
437
453
|
unstack(stackNumber) {
|
|
@@ -441,6 +457,13 @@ var GitHubCliPlatform = class {
|
|
|
441
457
|
String(stackNumber)
|
|
442
458
|
]);
|
|
443
459
|
}
|
|
460
|
+
closePullRequest(pr) {
|
|
461
|
+
this.gh([
|
|
462
|
+
"pr",
|
|
463
|
+
"close",
|
|
464
|
+
String(pr.number)
|
|
465
|
+
]);
|
|
466
|
+
}
|
|
444
467
|
editPullRequestBase(pr, base) {
|
|
445
468
|
this.gh([
|
|
446
469
|
"pr",
|
|
@@ -606,7 +629,6 @@ var Stack = class Stack {
|
|
|
606
629
|
const currentIds = this.changes.map((change) => change.id);
|
|
607
630
|
const previousIdSet = new Set(previousIds);
|
|
608
631
|
const currentIdSet = new Set(currentIds);
|
|
609
|
-
if (!sameSequence(previousIds.filter((id) => currentIdSet.has(id)), currentIds.filter((id) => previousIdSet.has(id)))) throw new Error("Submitted commits cannot be reordered. Restore their original relative order before syncing");
|
|
610
632
|
const removed = previous.changes.filter((change) => !currentIdSet.has(change.id));
|
|
611
633
|
const added = this.changes.filter((change) => !previousIdSet.has(change.id));
|
|
612
634
|
if (removed.length === 0) {
|
|
@@ -615,7 +637,7 @@ var Stack = class Stack {
|
|
|
615
637
|
return stackNumber === void 0 ? { kind: "full" } : {
|
|
616
638
|
kind: "rebuild",
|
|
617
639
|
stackNumber,
|
|
618
|
-
action: "insert"
|
|
640
|
+
action: added.length === 0 ? "reorder" : "insert"
|
|
619
641
|
};
|
|
620
642
|
}
|
|
621
643
|
const firstCurrentIndex = previousIds.indexOf(currentIds[0]);
|
|
@@ -651,9 +673,6 @@ var Stack = class Stack {
|
|
|
651
673
|
};
|
|
652
674
|
}
|
|
653
675
|
};
|
|
654
|
-
function sameSequence(left, right) {
|
|
655
|
-
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
656
|
-
}
|
|
657
676
|
//#endregion
|
|
658
677
|
//#region src/sync.ts
|
|
659
678
|
function syncStack(dependencies, options) {
|
|
@@ -690,24 +709,44 @@ function syncStack(dependencies, options) {
|
|
|
690
709
|
const state = stateStore.read();
|
|
691
710
|
const previous = stack.findPrevious(state);
|
|
692
711
|
const transition = stack.transitionFrom(previous, {
|
|
693
|
-
preserveHigherChanges: repository.currentBranch() ===
|
|
712
|
+
preserveHigherChanges: repository.currentBranch() === void 0,
|
|
694
713
|
lookups: {
|
|
695
714
|
pullRequestState: (pullRequest) => github.pullRequest(pullRequest).state,
|
|
696
715
|
stackNumberForPullRequest: (pullRequest) => github.stackNumberForPullRequest(pullRequest)
|
|
697
716
|
}
|
|
698
717
|
});
|
|
718
|
+
const isReorder = transition.kind === "rebuild" && transition.action === "reorder";
|
|
719
|
+
if (isReorder) {
|
|
720
|
+
reporter.progress(`Preparing stack #${transition.stackNumber} for reordered branches`);
|
|
721
|
+
github.unstack(transition.stackNumber);
|
|
722
|
+
try {
|
|
723
|
+
for (const change of previous.changes) github.editPullRequestBase(github.pullRequest(change.pullRequest), base);
|
|
724
|
+
} catch (error) {
|
|
725
|
+
restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
699
728
|
reporter.progress(`Pushing ${changes.length} remote branch${changes.length === 1 ? "" : "es"}`);
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
729
|
+
try {
|
|
730
|
+
repository.pushBranches(remote, changes.map((change) => ({
|
|
731
|
+
name: change.remoteBranch,
|
|
732
|
+
oid: change.oid
|
|
733
|
+
})));
|
|
734
|
+
} catch (error) {
|
|
735
|
+
if (isReorder) restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
736
|
+
throw error;
|
|
737
|
+
}
|
|
704
738
|
reporter.progress("Looking up existing pull requests");
|
|
705
739
|
const existing = changes.map((change) => github.pullRequestForBranch(change.remoteBranch));
|
|
706
|
-
|
|
740
|
+
const pullRequests = changes.map((change, index) => {
|
|
741
|
+
const current = existing[index];
|
|
742
|
+
if (current) return current;
|
|
743
|
+
const pullRequestBase = index === 0 ? base : changes[index - 1].remoteBranch;
|
|
744
|
+
reporter.progress(`Creating pull request: ${change.subject}`);
|
|
745
|
+
return github.createPullRequest(change, pullRequestBase, options.draft);
|
|
746
|
+
});
|
|
707
747
|
if (changes.length === 1) {
|
|
708
748
|
if (transition.kind === "partial") reporter.progress("Updating this down-stack prefix while preserving higher pull requests");
|
|
709
|
-
|
|
710
|
-
const pullRequest = existing[0] ?? github.createPullRequest(changes[0], base, options.draft);
|
|
749
|
+
const pullRequest = pullRequests[0];
|
|
711
750
|
if (transition.kind === "collapse") {
|
|
712
751
|
reporter.progress(`Removing omitted pull requests from stack #${transition.stackNumber}`);
|
|
713
752
|
github.unstack(transition.stackNumber);
|
|
@@ -717,29 +756,32 @@ function syncStack(dependencies, options) {
|
|
|
717
756
|
restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
718
757
|
}
|
|
719
758
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
github.
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
759
|
+
} else if (transition.kind === "full") {
|
|
760
|
+
reporter.progress(`Linking ${changes.length} pull requests as a native GitHub stack`);
|
|
761
|
+
github.linkStack(pullRequests.map((pullRequest) => pullRequest.number), base, remote, options.draft);
|
|
762
|
+
} else if (transition.kind === "rebuild") {
|
|
763
|
+
reporter.progress(`Rebuilding stack #${transition.stackNumber} to ${transition.action} pull requests`);
|
|
764
|
+
if (!isReorder) github.unstack(transition.stackNumber);
|
|
765
|
+
try {
|
|
766
|
+
github.linkStack(pullRequests.map((pullRequest) => pullRequest.number), base, remote, options.draft);
|
|
767
|
+
} catch (error) {
|
|
768
|
+
restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
769
|
+
}
|
|
770
|
+
} else if (transition.kind === "append") {
|
|
771
|
+
reporter.progress(`Appending ${transition.branches.length} pull request${transition.branches.length === 1 ? "" : "s"} to stack #${transition.stackNumber}`);
|
|
772
|
+
github.appendToStack(transition.stackNumber, transition.branches.map((branch) => {
|
|
773
|
+
const index = changes.findIndex((change) => change.remoteBranch === branch);
|
|
774
|
+
const pullRequest = pullRequests[index];
|
|
775
|
+
if (!pullRequest) throw new Error(`Missing pull request for ${branch}`);
|
|
776
|
+
return pullRequest.number;
|
|
777
|
+
}), remote, options.draft);
|
|
778
|
+
} else if (transition.kind === "partial") reporter.progress("Updating this down-stack prefix while preserving higher pull requests");
|
|
779
|
+
else reporter.progress("The native GitHub stack already has the correct members");
|
|
780
|
+
const currentIds = new Set(changes.map((change) => change.id));
|
|
781
|
+
const omittedPullRequests = transition.kind === "partial" ? [] : (previous?.changes ?? []).filter((change) => !currentIds.has(change.id)).map((change) => github.pullRequest(change.pullRequest)).filter((pullRequest) => pullRequest.state === "OPEN");
|
|
782
|
+
if (omittedPullRequests.length > 0) {
|
|
783
|
+
reporter.progress(`Closing ${omittedPullRequests.length} omitted pull request${omittedPullRequests.length === 1 ? "" : "s"}`);
|
|
784
|
+
for (const pullRequest of omittedPullRequests) github.closePullRequest(pullRequest);
|
|
743
785
|
}
|
|
744
786
|
reporter.progress("Synchronizing pull request titles and descriptions");
|
|
745
787
|
for (const [index, pr] of pullRequests.entries()) {
|
|
@@ -775,7 +817,7 @@ function restorePreviousStack(github, previous, base, remote, reporter, rebuildE
|
|
|
775
817
|
const rebuildMessage = rebuildError instanceof Error ? rebuildError.message : String(rebuildError);
|
|
776
818
|
reporter.progress("Rebuild failed; restoring the previous native GitHub stack");
|
|
777
819
|
try {
|
|
778
|
-
github.linkStack(previous.changes.map((change) => change.
|
|
820
|
+
github.linkStack(previous.changes.map((change) => change.pullRequest), base, remote, true);
|
|
779
821
|
} catch (rollbackError) {
|
|
780
822
|
const rollbackMessage = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
|
|
781
823
|
throw new Error(`Stack rebuild failed: ${rebuildMessage}\nRestoring the previous stack also failed: ${rollbackMessage}`);
|