bstack 1.1.6 → 1.2.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/README.md +5 -0
- package/dist/bstack.mjs +93 -64
- 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.
|
|
9
|
+
var version = "1.2.0";
|
|
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) {
|
|
@@ -606,7 +622,6 @@ var Stack = class Stack {
|
|
|
606
622
|
const currentIds = this.changes.map((change) => change.id);
|
|
607
623
|
const previousIdSet = new Set(previousIds);
|
|
608
624
|
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
625
|
const removed = previous.changes.filter((change) => !currentIdSet.has(change.id));
|
|
611
626
|
const added = this.changes.filter((change) => !previousIdSet.has(change.id));
|
|
612
627
|
if (removed.length === 0) {
|
|
@@ -615,7 +630,7 @@ var Stack = class Stack {
|
|
|
615
630
|
return stackNumber === void 0 ? { kind: "full" } : {
|
|
616
631
|
kind: "rebuild",
|
|
617
632
|
stackNumber,
|
|
618
|
-
action: "insert"
|
|
633
|
+
action: added.length === 0 ? "reorder" : "insert"
|
|
619
634
|
};
|
|
620
635
|
}
|
|
621
636
|
const firstCurrentIndex = previousIds.indexOf(currentIds[0]);
|
|
@@ -651,9 +666,6 @@ var Stack = class Stack {
|
|
|
651
666
|
};
|
|
652
667
|
}
|
|
653
668
|
};
|
|
654
|
-
function sameSequence(left, right) {
|
|
655
|
-
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
656
|
-
}
|
|
657
669
|
//#endregion
|
|
658
670
|
//#region src/sync.ts
|
|
659
671
|
function syncStack(dependencies, options) {
|
|
@@ -690,24 +702,44 @@ function syncStack(dependencies, options) {
|
|
|
690
702
|
const state = stateStore.read();
|
|
691
703
|
const previous = stack.findPrevious(state);
|
|
692
704
|
const transition = stack.transitionFrom(previous, {
|
|
693
|
-
preserveHigherChanges: repository.currentBranch() ===
|
|
705
|
+
preserveHigherChanges: repository.currentBranch() === void 0,
|
|
694
706
|
lookups: {
|
|
695
707
|
pullRequestState: (pullRequest) => github.pullRequest(pullRequest).state,
|
|
696
708
|
stackNumberForPullRequest: (pullRequest) => github.stackNumberForPullRequest(pullRequest)
|
|
697
709
|
}
|
|
698
710
|
});
|
|
711
|
+
const isReorder = transition.kind === "rebuild" && transition.action === "reorder";
|
|
712
|
+
if (isReorder) {
|
|
713
|
+
reporter.progress(`Preparing stack #${transition.stackNumber} for reordered branches`);
|
|
714
|
+
github.unstack(transition.stackNumber);
|
|
715
|
+
try {
|
|
716
|
+
for (const change of previous.changes) github.editPullRequestBase(github.pullRequest(change.pullRequest), base);
|
|
717
|
+
} catch (error) {
|
|
718
|
+
restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
699
721
|
reporter.progress(`Pushing ${changes.length} remote branch${changes.length === 1 ? "" : "es"}`);
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
722
|
+
try {
|
|
723
|
+
repository.pushBranches(remote, changes.map((change) => ({
|
|
724
|
+
name: change.remoteBranch,
|
|
725
|
+
oid: change.oid
|
|
726
|
+
})));
|
|
727
|
+
} catch (error) {
|
|
728
|
+
if (isReorder) restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
729
|
+
throw error;
|
|
730
|
+
}
|
|
704
731
|
reporter.progress("Looking up existing pull requests");
|
|
705
732
|
const existing = changes.map((change) => github.pullRequestForBranch(change.remoteBranch));
|
|
706
|
-
|
|
733
|
+
const pullRequests = changes.map((change, index) => {
|
|
734
|
+
const current = existing[index];
|
|
735
|
+
if (current) return current;
|
|
736
|
+
const pullRequestBase = index === 0 ? base : changes[index - 1].remoteBranch;
|
|
737
|
+
reporter.progress(`Creating pull request: ${change.subject}`);
|
|
738
|
+
return github.createPullRequest(change, pullRequestBase, options.draft);
|
|
739
|
+
});
|
|
707
740
|
if (changes.length === 1) {
|
|
708
741
|
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);
|
|
742
|
+
const pullRequest = pullRequests[0];
|
|
711
743
|
if (transition.kind === "collapse") {
|
|
712
744
|
reporter.progress(`Removing omitted pull requests from stack #${transition.stackNumber}`);
|
|
713
745
|
github.unstack(transition.stackNumber);
|
|
@@ -717,30 +749,27 @@ function syncStack(dependencies, options) {
|
|
|
717
749
|
restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
718
750
|
}
|
|
719
751
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
github.
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
return pr;
|
|
742
|
-
});
|
|
743
|
-
}
|
|
752
|
+
} else if (transition.kind === "full") {
|
|
753
|
+
reporter.progress(`Linking ${changes.length} pull requests as a native GitHub stack`);
|
|
754
|
+
github.linkStack(pullRequests.map((pullRequest) => pullRequest.number), base, remote, options.draft);
|
|
755
|
+
} else if (transition.kind === "rebuild") {
|
|
756
|
+
reporter.progress(`Rebuilding stack #${transition.stackNumber} to ${transition.action} pull requests`);
|
|
757
|
+
if (!isReorder) github.unstack(transition.stackNumber);
|
|
758
|
+
try {
|
|
759
|
+
github.linkStack(pullRequests.map((pullRequest) => pullRequest.number), base, remote, options.draft);
|
|
760
|
+
} catch (error) {
|
|
761
|
+
restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
762
|
+
}
|
|
763
|
+
} else if (transition.kind === "append") {
|
|
764
|
+
reporter.progress(`Appending ${transition.branches.length} pull request${transition.branches.length === 1 ? "" : "s"} to stack #${transition.stackNumber}`);
|
|
765
|
+
github.appendToStack(transition.stackNumber, transition.branches.map((branch) => {
|
|
766
|
+
const index = changes.findIndex((change) => change.remoteBranch === branch);
|
|
767
|
+
const pullRequest = pullRequests[index];
|
|
768
|
+
if (!pullRequest) throw new Error(`Missing pull request for ${branch}`);
|
|
769
|
+
return pullRequest.number;
|
|
770
|
+
}), remote, options.draft);
|
|
771
|
+
} else if (transition.kind === "partial") reporter.progress("Updating this down-stack prefix while preserving higher pull requests");
|
|
772
|
+
else reporter.progress("The native GitHub stack already has the correct members");
|
|
744
773
|
reporter.progress("Synchronizing pull request titles and descriptions");
|
|
745
774
|
for (const [index, pr] of pullRequests.entries()) {
|
|
746
775
|
github.editPullRequest(pr, changes[index]);
|
|
@@ -775,7 +804,7 @@ function restorePreviousStack(github, previous, base, remote, reporter, rebuildE
|
|
|
775
804
|
const rebuildMessage = rebuildError instanceof Error ? rebuildError.message : String(rebuildError);
|
|
776
805
|
reporter.progress("Rebuild failed; restoring the previous native GitHub stack");
|
|
777
806
|
try {
|
|
778
|
-
github.linkStack(previous.changes.map((change) => change.
|
|
807
|
+
github.linkStack(previous.changes.map((change) => change.pullRequest), base, remote, true);
|
|
779
808
|
} catch (rollbackError) {
|
|
780
809
|
const rollbackMessage = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
|
|
781
810
|
throw new Error(`Stack rebuild failed: ${rebuildMessage}\nRestoring the previous stack also failed: ${rollbackMessage}`);
|