bstack 1.1.5 → 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 +94 -62
- 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,6 +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
|
+
if (!repository.isClean()) throw new Error("The working tree must be clean before checkout");
|
|
62
63
|
github.assertReady();
|
|
63
64
|
const remote = repository.resolveRemote(options.remote);
|
|
64
65
|
reporter.progress(`Looking up pull request ${options.reference}`);
|
|
@@ -166,7 +167,9 @@ var GitCliRepository = class {
|
|
|
166
167
|
}
|
|
167
168
|
assertReady() {
|
|
168
169
|
this.git(["rev-parse", "--show-toplevel"]);
|
|
169
|
-
|
|
170
|
+
}
|
|
171
|
+
isClean() {
|
|
172
|
+
return this.git(["status", "--porcelain"]).stdout.trim() === "";
|
|
170
173
|
}
|
|
171
174
|
currentBranch() {
|
|
172
175
|
return this.git([
|
|
@@ -174,7 +177,7 @@ var GitCliRepository = class {
|
|
|
174
177
|
"--quiet",
|
|
175
178
|
"--short",
|
|
176
179
|
"HEAD"
|
|
177
|
-
], { allowFailure: true }).stdout.trim();
|
|
180
|
+
], { allowFailure: true }).stdout.trim() || void 0;
|
|
178
181
|
}
|
|
179
182
|
resolveRemote(requested) {
|
|
180
183
|
if (requested) {
|
|
@@ -324,6 +327,14 @@ const pullRequestSchema = v.object({
|
|
|
324
327
|
body: v.string(),
|
|
325
328
|
isDraft: v.boolean()
|
|
326
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
|
+
});
|
|
327
338
|
const stackSchema = v.object({ number: v.number() });
|
|
328
339
|
var GitHubCliPlatform = class {
|
|
329
340
|
cwd;
|
|
@@ -389,25 +400,33 @@ var GitHubCliPlatform = class {
|
|
|
389
400
|
return v.parse(pullRequestSchema, JSON.parse(raw));
|
|
390
401
|
}
|
|
391
402
|
createPullRequest(change, base, draft) {
|
|
392
|
-
const
|
|
393
|
-
"
|
|
394
|
-
"
|
|
395
|
-
"
|
|
396
|
-
|
|
397
|
-
"--
|
|
398
|
-
|
|
399
|
-
"--
|
|
400
|
-
change.
|
|
401
|
-
"--
|
|
402
|
-
change.
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
+
};
|
|
409
428
|
}
|
|
410
|
-
linkStack(
|
|
429
|
+
linkStack(pullRequests, base, remote, draft) {
|
|
411
430
|
const args = [
|
|
412
431
|
"stack",
|
|
413
432
|
"link",
|
|
@@ -417,10 +436,10 @@ var GitHubCliPlatform = class {
|
|
|
417
436
|
remote
|
|
418
437
|
];
|
|
419
438
|
if (!draft) args.push("--open");
|
|
420
|
-
args.push(...
|
|
439
|
+
args.push(...pullRequests.map(String));
|
|
421
440
|
this.gh(args);
|
|
422
441
|
}
|
|
423
|
-
appendToStack(stackNumber,
|
|
442
|
+
appendToStack(stackNumber, pullRequests, remote, draft) {
|
|
424
443
|
const args = [
|
|
425
444
|
"stack",
|
|
426
445
|
"link",
|
|
@@ -428,7 +447,7 @@ var GitHubCliPlatform = class {
|
|
|
428
447
|
remote
|
|
429
448
|
];
|
|
430
449
|
if (!draft) args.push("--open");
|
|
431
|
-
args.push(String(stackNumber), ...
|
|
450
|
+
args.push(String(stackNumber), ...pullRequests.map(String));
|
|
432
451
|
this.gh(args);
|
|
433
452
|
}
|
|
434
453
|
unstack(stackNumber) {
|
|
@@ -603,7 +622,6 @@ var Stack = class Stack {
|
|
|
603
622
|
const currentIds = this.changes.map((change) => change.id);
|
|
604
623
|
const previousIdSet = new Set(previousIds);
|
|
605
624
|
const currentIdSet = new Set(currentIds);
|
|
606
|
-
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");
|
|
607
625
|
const removed = previous.changes.filter((change) => !currentIdSet.has(change.id));
|
|
608
626
|
const added = this.changes.filter((change) => !previousIdSet.has(change.id));
|
|
609
627
|
if (removed.length === 0) {
|
|
@@ -612,7 +630,7 @@ var Stack = class Stack {
|
|
|
612
630
|
return stackNumber === void 0 ? { kind: "full" } : {
|
|
613
631
|
kind: "rebuild",
|
|
614
632
|
stackNumber,
|
|
615
|
-
action: "insert"
|
|
633
|
+
action: added.length === 0 ? "reorder" : "insert"
|
|
616
634
|
};
|
|
617
635
|
}
|
|
618
636
|
const firstCurrentIndex = previousIds.indexOf(currentIds[0]);
|
|
@@ -648,9 +666,6 @@ var Stack = class Stack {
|
|
|
648
666
|
};
|
|
649
667
|
}
|
|
650
668
|
};
|
|
651
|
-
function sameSequence(left, right) {
|
|
652
|
-
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
653
|
-
}
|
|
654
669
|
//#endregion
|
|
655
670
|
//#region src/sync.ts
|
|
656
671
|
function syncStack(dependencies, options) {
|
|
@@ -687,24 +702,44 @@ function syncStack(dependencies, options) {
|
|
|
687
702
|
const state = stateStore.read();
|
|
688
703
|
const previous = stack.findPrevious(state);
|
|
689
704
|
const transition = stack.transitionFrom(previous, {
|
|
690
|
-
preserveHigherChanges: repository.currentBranch() ===
|
|
705
|
+
preserveHigherChanges: repository.currentBranch() === void 0,
|
|
691
706
|
lookups: {
|
|
692
707
|
pullRequestState: (pullRequest) => github.pullRequest(pullRequest).state,
|
|
693
708
|
stackNumberForPullRequest: (pullRequest) => github.stackNumberForPullRequest(pullRequest)
|
|
694
709
|
}
|
|
695
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
|
+
}
|
|
696
721
|
reporter.progress(`Pushing ${changes.length} remote branch${changes.length === 1 ? "" : "es"}`);
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
+
}
|
|
701
731
|
reporter.progress("Looking up existing pull requests");
|
|
702
732
|
const existing = changes.map((change) => github.pullRequestForBranch(change.remoteBranch));
|
|
703
|
-
|
|
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
|
+
});
|
|
704
740
|
if (changes.length === 1) {
|
|
705
741
|
if (transition.kind === "partial") reporter.progress("Updating this down-stack prefix while preserving higher pull requests");
|
|
706
|
-
|
|
707
|
-
const pullRequest = existing[0] ?? github.createPullRequest(changes[0], base, options.draft);
|
|
742
|
+
const pullRequest = pullRequests[0];
|
|
708
743
|
if (transition.kind === "collapse") {
|
|
709
744
|
reporter.progress(`Removing omitted pull requests from stack #${transition.stackNumber}`);
|
|
710
745
|
github.unstack(transition.stackNumber);
|
|
@@ -714,30 +749,27 @@ function syncStack(dependencies, options) {
|
|
|
714
749
|
restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
715
750
|
}
|
|
716
751
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
github.
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
return pr;
|
|
739
|
-
});
|
|
740
|
-
}
|
|
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");
|
|
741
773
|
reporter.progress("Synchronizing pull request titles and descriptions");
|
|
742
774
|
for (const [index, pr] of pullRequests.entries()) {
|
|
743
775
|
github.editPullRequest(pr, changes[index]);
|
|
@@ -772,7 +804,7 @@ function restorePreviousStack(github, previous, base, remote, reporter, rebuildE
|
|
|
772
804
|
const rebuildMessage = rebuildError instanceof Error ? rebuildError.message : String(rebuildError);
|
|
773
805
|
reporter.progress("Rebuild failed; restoring the previous native GitHub stack");
|
|
774
806
|
try {
|
|
775
|
-
github.linkStack(previous.changes.map((change) => change.
|
|
807
|
+
github.linkStack(previous.changes.map((change) => change.pullRequest), base, remote, true);
|
|
776
808
|
} catch (rollbackError) {
|
|
777
809
|
const rollbackMessage = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
|
|
778
810
|
throw new Error(`Stack rebuild failed: ${rebuildMessage}\nRestoring the previous stack also failed: ${rollbackMessage}`);
|