bstack 1.4.0 → 1.5.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/dist/bstack.mjs +107 -59
- package/package.json +2 -1
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.5.0";
|
|
10
10
|
//#endregion
|
|
11
11
|
//#region src/command.ts
|
|
12
12
|
var CommandError = class extends Error {
|
|
@@ -265,9 +265,14 @@ var GitCliRepository = class {
|
|
|
265
265
|
}
|
|
266
266
|
pushBranches(remote, branches) {
|
|
267
267
|
const existing = this.remoteBranchOids(remote, branches.map((branch) => branch.name));
|
|
268
|
+
const updates = branches.filter((branch) => existing.get(branch.name) !== branch.oid);
|
|
269
|
+
if (updates.length === 0) return {
|
|
270
|
+
checked: branches.length,
|
|
271
|
+
updated: []
|
|
272
|
+
};
|
|
268
273
|
const leases = [];
|
|
269
274
|
const refspecs = [];
|
|
270
|
-
for (const branch of
|
|
275
|
+
for (const branch of updates) {
|
|
271
276
|
const expected = existing.get(branch.name) ?? "";
|
|
272
277
|
leases.push(`--force-with-lease=refs/heads/${branch.name}:${expected}`);
|
|
273
278
|
refspecs.push(`${branch.oid}:refs/heads/${branch.name}`);
|
|
@@ -279,7 +284,10 @@ var GitCliRepository = class {
|
|
|
279
284
|
remote,
|
|
280
285
|
...refspecs
|
|
281
286
|
]);
|
|
282
|
-
return
|
|
287
|
+
return {
|
|
288
|
+
checked: branches.length,
|
|
289
|
+
updated: updates.map((branch) => branch.name)
|
|
290
|
+
};
|
|
283
291
|
}
|
|
284
292
|
statePath() {
|
|
285
293
|
return this.git([
|
|
@@ -513,7 +521,7 @@ var GitHubCliPlatform = class {
|
|
|
513
521
|
//#region src/output.ts
|
|
514
522
|
function formatSyncResult(result, dryRun) {
|
|
515
523
|
const changeCount = `${result.changes.length} change${result.changes.length === 1 ? "" : "s"}`;
|
|
516
|
-
const lines = [dryRun ? `Would sync ${changeCount} against ${result.base}:` : `Synced stack against ${result.base}
|
|
524
|
+
const lines = [dryRun ? `Would sync ${changeCount} against ${result.base}:` : `Synced ${result.changes.length}-commit stack against ${result.base}:`];
|
|
517
525
|
if (dryRun) {
|
|
518
526
|
for (const change of result.changes) lines.push(` ${change.oid.slice(0, 8)} ${change.subject}`);
|
|
519
527
|
return lines.join("\n");
|
|
@@ -603,6 +611,13 @@ var Stack = class Stack {
|
|
|
603
611
|
this.rewritten = rewritten;
|
|
604
612
|
this.rewrites = rewrites;
|
|
605
613
|
if (changes.length === 0) throw new Error("A stack must contain at least one change");
|
|
614
|
+
const seen = /* @__PURE__ */ new Set();
|
|
615
|
+
const duplicate = changes.find((change) => {
|
|
616
|
+
if (seen.has(change.id)) return true;
|
|
617
|
+
seen.add(change.id);
|
|
618
|
+
return false;
|
|
619
|
+
});
|
|
620
|
+
if (duplicate) throw new Error(`A stack cannot contain duplicate bstack-id ${duplicate.id}`);
|
|
606
621
|
}
|
|
607
622
|
static fromChanges(changes) {
|
|
608
623
|
return new Stack(changes);
|
|
@@ -652,7 +667,17 @@ var Stack = class Stack {
|
|
|
652
667
|
const removed = previous.changes.filter((change) => !currentIdSet.has(change.id));
|
|
653
668
|
const added = this.changes.filter((change) => !previousIdSet.has(change.id));
|
|
654
669
|
if (removed.length === 0) {
|
|
655
|
-
|
|
670
|
+
const previousIsPrefix = previousIds.every((id, index) => currentIds[index] === id);
|
|
671
|
+
if (previousIsPrefix && added.length === 0) return { kind: "skip" };
|
|
672
|
+
if (previousIsPrefix) {
|
|
673
|
+
const stackNumber = previous.stackNumber ?? options.lookups.stackNumberForPullRequest(previous.changes[0].pullRequest);
|
|
674
|
+
if (stackNumber !== void 0) return {
|
|
675
|
+
kind: "append",
|
|
676
|
+
stackNumber,
|
|
677
|
+
branches: added.map((change) => change.remoteBranch)
|
|
678
|
+
};
|
|
679
|
+
return { kind: "full" };
|
|
680
|
+
}
|
|
656
681
|
const stackNumber = previous.stackNumber ?? options.lookups.stackNumberForPullRequest(previous.changes[0].pullRequest);
|
|
657
682
|
return stackNumber === void 0 ? { kind: "full" } : {
|
|
658
683
|
kind: "rebuild",
|
|
@@ -694,6 +719,73 @@ var Stack = class Stack {
|
|
|
694
719
|
}
|
|
695
720
|
};
|
|
696
721
|
//#endregion
|
|
722
|
+
//#region src/sync-transition.ts
|
|
723
|
+
function prepareStackTransition(transition, github, previous, base, remote, reporter) {
|
|
724
|
+
if (transition.kind !== "rebuild" || transition.action !== "reorder") return;
|
|
725
|
+
reporter.progress(`Preparing stack #${transition.stackNumber} for reordered branches`);
|
|
726
|
+
github.unstack(transition.stackNumber);
|
|
727
|
+
try {
|
|
728
|
+
for (const change of previous.changes) github.editPullRequestBase(github.pullRequest(change.pullRequest), base);
|
|
729
|
+
} catch (error) {
|
|
730
|
+
restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
function applyStackTransition(transition, github, previous, changes, pullRequests, base, remote, draft, reporter) {
|
|
734
|
+
if (changes.length === 1) {
|
|
735
|
+
if (transition.kind === "partial") reporter.progress("Updating this down-stack prefix while preserving higher pull requests");
|
|
736
|
+
if (transition.kind === "collapse") {
|
|
737
|
+
reporter.progress(`Removing omitted pull requests from stack #${transition.stackNumber}`);
|
|
738
|
+
github.unstack(transition.stackNumber);
|
|
739
|
+
try {
|
|
740
|
+
github.editPullRequestBase(pullRequests[0], base);
|
|
741
|
+
} catch (error) {
|
|
742
|
+
restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (transition.kind === "full") {
|
|
748
|
+
reporter.progress(`Linking ${changes.length} pull requests as a native GitHub stack`);
|
|
749
|
+
github.linkStack(pullRequests.map((pullRequest) => pullRequest.number), base, remote, draft);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (transition.kind === "rebuild") {
|
|
753
|
+
reporter.progress(`Rebuilding stack #${transition.stackNumber} to ${transition.action} pull requests`);
|
|
754
|
+
if (transition.action !== "reorder") github.unstack(transition.stackNumber);
|
|
755
|
+
try {
|
|
756
|
+
github.linkStack(pullRequests.map((pullRequest) => pullRequest.number), base, remote, draft);
|
|
757
|
+
} catch (error) {
|
|
758
|
+
restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
759
|
+
}
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (transition.kind === "append") {
|
|
763
|
+
reporter.progress(`Appending ${transition.branches.length} pull request${transition.branches.length === 1 ? "" : "s"} to stack #${transition.stackNumber}`);
|
|
764
|
+
github.appendToStack(transition.stackNumber, transition.branches.map((branch) => {
|
|
765
|
+
const pullRequest = pullRequests[changes.findIndex((change) => change.remoteBranch === branch)];
|
|
766
|
+
if (!pullRequest) throw new Error(`Missing pull request for ${branch}`);
|
|
767
|
+
return pullRequest.number;
|
|
768
|
+
}), remote, draft);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
if (transition.kind === "partial") {
|
|
772
|
+
reporter.progress("Updating this down-stack prefix while preserving higher pull requests");
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
reporter.progress("The native GitHub stack already has the correct members");
|
|
776
|
+
}
|
|
777
|
+
function restorePreviousStack(github, previous, base, remote, reporter, rebuildError) {
|
|
778
|
+
const rebuildMessage = rebuildError instanceof Error ? rebuildError.message : String(rebuildError);
|
|
779
|
+
reporter.progress("Rebuild failed; restoring the previous native GitHub stack");
|
|
780
|
+
try {
|
|
781
|
+
github.linkStack(previous.changes.map((change) => change.pullRequest), base, remote, true);
|
|
782
|
+
} catch (rollbackError) {
|
|
783
|
+
const rollbackMessage = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
|
|
784
|
+
throw new Error(`Stack rebuild failed: ${rebuildMessage}\nRestoring the previous stack also failed: ${rollbackMessage}`);
|
|
785
|
+
}
|
|
786
|
+
throw rebuildError;
|
|
787
|
+
}
|
|
788
|
+
//#endregion
|
|
697
789
|
//#region src/sync.ts
|
|
698
790
|
function syncStack(dependencies, options) {
|
|
699
791
|
const { repository, github, stateStore, reporter } = dependencies;
|
|
@@ -736,24 +828,16 @@ function syncStack(dependencies, options) {
|
|
|
736
828
|
stackNumberForPullRequest: (pullRequest) => github.stackNumberForPullRequest(pullRequest)
|
|
737
829
|
}
|
|
738
830
|
});
|
|
831
|
+
prepareStackTransition(transition, github, previous, base, remote, reporter);
|
|
739
832
|
const isReorder = transition.kind === "rebuild" && transition.action === "reorder";
|
|
740
|
-
if (isReorder) {
|
|
741
|
-
reporter.progress(`Preparing stack #${transition.stackNumber} for reordered branches`);
|
|
742
|
-
github.unstack(transition.stackNumber);
|
|
743
|
-
try {
|
|
744
|
-
for (const change of previous.changes) github.editPullRequestBase(github.pullRequest(change.pullRequest), base);
|
|
745
|
-
} catch (error) {
|
|
746
|
-
restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
reporter.progress(`Pushing ${changes.length} remote branch${changes.length === 1 ? "" : "es"}`);
|
|
750
833
|
let pushedBranches = /* @__PURE__ */ new Set();
|
|
751
834
|
try {
|
|
752
|
-
const
|
|
835
|
+
const pushResult = repository.pushBranches(remote, changes.map((change) => ({
|
|
753
836
|
name: change.remoteBranch,
|
|
754
837
|
oid: change.oid
|
|
755
838
|
})));
|
|
756
|
-
|
|
839
|
+
reportPushResult(reporter, pushResult);
|
|
840
|
+
pushedBranches = new Set(pushResult.updated);
|
|
757
841
|
} catch (error) {
|
|
758
842
|
if (isReorder) restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
759
843
|
throw error;
|
|
@@ -768,39 +852,7 @@ function syncStack(dependencies, options) {
|
|
|
768
852
|
return github.createPullRequest(change, pullRequestBase, options.draft);
|
|
769
853
|
});
|
|
770
854
|
const createdBranches = new Set(changes.filter((_change, index) => existing[index] === void 0).map((change) => change.remoteBranch));
|
|
771
|
-
|
|
772
|
-
if (transition.kind === "partial") reporter.progress("Updating this down-stack prefix while preserving higher pull requests");
|
|
773
|
-
const pullRequest = pullRequests[0];
|
|
774
|
-
if (transition.kind === "collapse") {
|
|
775
|
-
reporter.progress(`Removing omitted pull requests from stack #${transition.stackNumber}`);
|
|
776
|
-
github.unstack(transition.stackNumber);
|
|
777
|
-
try {
|
|
778
|
-
github.editPullRequestBase(pullRequest, base);
|
|
779
|
-
} catch (error) {
|
|
780
|
-
restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
} else if (transition.kind === "full") {
|
|
784
|
-
reporter.progress(`Linking ${changes.length} pull requests as a native GitHub stack`);
|
|
785
|
-
github.linkStack(pullRequests.map((pullRequest) => pullRequest.number), base, remote, options.draft);
|
|
786
|
-
} else if (transition.kind === "rebuild") {
|
|
787
|
-
reporter.progress(`Rebuilding stack #${transition.stackNumber} to ${transition.action} pull requests`);
|
|
788
|
-
if (!isReorder) github.unstack(transition.stackNumber);
|
|
789
|
-
try {
|
|
790
|
-
github.linkStack(pullRequests.map((pullRequest) => pullRequest.number), base, remote, options.draft);
|
|
791
|
-
} catch (error) {
|
|
792
|
-
restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
793
|
-
}
|
|
794
|
-
} else if (transition.kind === "append") {
|
|
795
|
-
reporter.progress(`Appending ${transition.branches.length} pull request${transition.branches.length === 1 ? "" : "s"} to stack #${transition.stackNumber}`);
|
|
796
|
-
github.appendToStack(transition.stackNumber, transition.branches.map((branch) => {
|
|
797
|
-
const index = changes.findIndex((change) => change.remoteBranch === branch);
|
|
798
|
-
const pullRequest = pullRequests[index];
|
|
799
|
-
if (!pullRequest) throw new Error(`Missing pull request for ${branch}`);
|
|
800
|
-
return pullRequest.number;
|
|
801
|
-
}), remote, options.draft);
|
|
802
|
-
} else if (transition.kind === "partial") reporter.progress("Updating this down-stack prefix while preserving higher pull requests");
|
|
803
|
-
else reporter.progress("The native GitHub stack already has the correct members");
|
|
855
|
+
applyStackTransition(transition, github, previous, changes, pullRequests, base, remote, options.draft, reporter);
|
|
804
856
|
const currentIds = new Set(changes.map((change) => change.id));
|
|
805
857
|
const omittedPullRequests = transition.kind === "partial" ? [] : (previous?.changes ?? []).filter((change) => !currentIds.has(change.id)).map((change) => github.pullRequest(change.pullRequest)).filter((pullRequest) => pullRequest.state === "OPEN");
|
|
806
858
|
if (omittedPullRequests.length > 0) {
|
|
@@ -856,16 +908,12 @@ function changeOutcome(change, index, pullRequest, previous, changes, base, crea
|
|
|
856
908
|
const metadataChanged = pullRequest.title !== change.subject || pullRequest.body !== change.body;
|
|
857
909
|
return pushedBranches.has(change.remoteBranch) || previousBase !== currentBase || metadataChanged ? "updated" : "unchanged";
|
|
858
910
|
}
|
|
859
|
-
function
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
github.linkStack(previous.changes.map((change) => change.pullRequest), base, remote, true);
|
|
864
|
-
} catch (rollbackError) {
|
|
865
|
-
const rollbackMessage = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
|
|
866
|
-
throw new Error(`Stack rebuild failed: ${rebuildMessage}\nRestoring the previous stack also failed: ${rollbackMessage}`);
|
|
911
|
+
function reportPushResult(reporter, result) {
|
|
912
|
+
if (result.updated.length === 0) {
|
|
913
|
+
reporter.progress(`All ${result.checked} remote branch${result.checked === 1 ? "" : "es"} already match`);
|
|
914
|
+
return;
|
|
867
915
|
}
|
|
868
|
-
|
|
916
|
+
reporter.progress(`Updating ${result.updated.length} of ${result.checked} remote branch${result.checked === 1 ? "" : "es"}`);
|
|
869
917
|
}
|
|
870
918
|
function writeUpdatedState(store, state, previous, updated) {
|
|
871
919
|
const stacks = previous ? state.stacks.map((stack) => stack === previous ? updated : stack) : [...state.stacks, updated];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bstack",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Create native GitHub stacked pull requests from a linear series of commits",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"valibot": "^1.4.2"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
+
"@total-typescript/shoehorn": "^0.1.2",
|
|
35
36
|
"@types/node": "^26.0.0",
|
|
36
37
|
"oxfmt": "^0.64.0",
|
|
37
38
|
"oxlint": "^1.79.0",
|