bstack 1.3.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 +143 -71
- 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([
|
|
@@ -510,6 +518,25 @@ var GitHubCliPlatform = class {
|
|
|
510
518
|
}
|
|
511
519
|
};
|
|
512
520
|
//#endregion
|
|
521
|
+
//#region src/output.ts
|
|
522
|
+
function formatSyncResult(result, dryRun) {
|
|
523
|
+
const changeCount = `${result.changes.length} change${result.changes.length === 1 ? "" : "s"}`;
|
|
524
|
+
const lines = [dryRun ? `Would sync ${changeCount} against ${result.base}:` : `Synced ${result.changes.length}-commit stack against ${result.base}:`];
|
|
525
|
+
if (dryRun) {
|
|
526
|
+
for (const change of result.changes) lines.push(` ${change.oid.slice(0, 8)} ${change.subject}`);
|
|
527
|
+
return lines.join("\n");
|
|
528
|
+
}
|
|
529
|
+
for (const outcome of result.outcomes) {
|
|
530
|
+
if (outcome.outcome === "closed") {
|
|
531
|
+
const { pullRequest } = outcome;
|
|
532
|
+
lines.push(` ${outcome.outcome.padEnd(9)} #${pullRequest.number} ${pullRequest.title} ${pullRequest.url}`);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
lines.push(` ${outcome.outcome.padEnd(9)} #${outcome.pullRequest.number} ${outcome.change.subject} ${outcome.pullRequest.url}`);
|
|
536
|
+
}
|
|
537
|
+
return lines.join("\n");
|
|
538
|
+
}
|
|
539
|
+
//#endregion
|
|
513
540
|
//#region src/reporter.ts
|
|
514
541
|
var ConsoleReporter = class {
|
|
515
542
|
progress(message) {
|
|
@@ -584,6 +611,13 @@ var Stack = class Stack {
|
|
|
584
611
|
this.rewritten = rewritten;
|
|
585
612
|
this.rewrites = rewrites;
|
|
586
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}`);
|
|
587
621
|
}
|
|
588
622
|
static fromChanges(changes) {
|
|
589
623
|
return new Stack(changes);
|
|
@@ -633,7 +667,17 @@ var Stack = class Stack {
|
|
|
633
667
|
const removed = previous.changes.filter((change) => !currentIdSet.has(change.id));
|
|
634
668
|
const added = this.changes.filter((change) => !previousIdSet.has(change.id));
|
|
635
669
|
if (removed.length === 0) {
|
|
636
|
-
|
|
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
|
+
}
|
|
637
681
|
const stackNumber = previous.stackNumber ?? options.lookups.stackNumberForPullRequest(previous.changes[0].pullRequest);
|
|
638
682
|
return stackNumber === void 0 ? { kind: "full" } : {
|
|
639
683
|
kind: "rebuild",
|
|
@@ -675,6 +719,73 @@ var Stack = class Stack {
|
|
|
675
719
|
}
|
|
676
720
|
};
|
|
677
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
|
|
678
789
|
//#region src/sync.ts
|
|
679
790
|
function syncStack(dependencies, options) {
|
|
680
791
|
const { repository, github, stateStore, reporter } = dependencies;
|
|
@@ -703,7 +814,8 @@ function syncStack(dependencies, options) {
|
|
|
703
814
|
base,
|
|
704
815
|
remote,
|
|
705
816
|
rewritten,
|
|
706
|
-
changes: [...changes]
|
|
817
|
+
changes: [...changes],
|
|
818
|
+
outcomes: []
|
|
707
819
|
};
|
|
708
820
|
}
|
|
709
821
|
reporter.progress("Reading the previous stack state");
|
|
@@ -716,24 +828,16 @@ function syncStack(dependencies, options) {
|
|
|
716
828
|
stackNumberForPullRequest: (pullRequest) => github.stackNumberForPullRequest(pullRequest)
|
|
717
829
|
}
|
|
718
830
|
});
|
|
831
|
+
prepareStackTransition(transition, github, previous, base, remote, reporter);
|
|
719
832
|
const isReorder = transition.kind === "rebuild" && transition.action === "reorder";
|
|
720
|
-
if (isReorder) {
|
|
721
|
-
reporter.progress(`Preparing stack #${transition.stackNumber} for reordered branches`);
|
|
722
|
-
github.unstack(transition.stackNumber);
|
|
723
|
-
try {
|
|
724
|
-
for (const change of previous.changes) github.editPullRequestBase(github.pullRequest(change.pullRequest), base);
|
|
725
|
-
} catch (error) {
|
|
726
|
-
restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
reporter.progress(`Pushing ${changes.length} remote branch${changes.length === 1 ? "" : "es"}`);
|
|
730
833
|
let pushedBranches = /* @__PURE__ */ new Set();
|
|
731
834
|
try {
|
|
732
|
-
const
|
|
835
|
+
const pushResult = repository.pushBranches(remote, changes.map((change) => ({
|
|
733
836
|
name: change.remoteBranch,
|
|
734
837
|
oid: change.oid
|
|
735
838
|
})));
|
|
736
|
-
|
|
839
|
+
reportPushResult(reporter, pushResult);
|
|
840
|
+
pushedBranches = new Set(pushResult.updated);
|
|
737
841
|
} catch (error) {
|
|
738
842
|
if (isReorder) restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
739
843
|
throw error;
|
|
@@ -748,39 +852,7 @@ function syncStack(dependencies, options) {
|
|
|
748
852
|
return github.createPullRequest(change, pullRequestBase, options.draft);
|
|
749
853
|
});
|
|
750
854
|
const createdBranches = new Set(changes.filter((_change, index) => existing[index] === void 0).map((change) => change.remoteBranch));
|
|
751
|
-
|
|
752
|
-
if (transition.kind === "partial") reporter.progress("Updating this down-stack prefix while preserving higher pull requests");
|
|
753
|
-
const pullRequest = pullRequests[0];
|
|
754
|
-
if (transition.kind === "collapse") {
|
|
755
|
-
reporter.progress(`Removing omitted pull requests from stack #${transition.stackNumber}`);
|
|
756
|
-
github.unstack(transition.stackNumber);
|
|
757
|
-
try {
|
|
758
|
-
github.editPullRequestBase(pullRequest, base);
|
|
759
|
-
} catch (error) {
|
|
760
|
-
restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
} else if (transition.kind === "full") {
|
|
764
|
-
reporter.progress(`Linking ${changes.length} pull requests as a native GitHub stack`);
|
|
765
|
-
github.linkStack(pullRequests.map((pullRequest) => pullRequest.number), base, remote, options.draft);
|
|
766
|
-
} else if (transition.kind === "rebuild") {
|
|
767
|
-
reporter.progress(`Rebuilding stack #${transition.stackNumber} to ${transition.action} pull requests`);
|
|
768
|
-
if (!isReorder) github.unstack(transition.stackNumber);
|
|
769
|
-
try {
|
|
770
|
-
github.linkStack(pullRequests.map((pullRequest) => pullRequest.number), base, remote, options.draft);
|
|
771
|
-
} catch (error) {
|
|
772
|
-
restorePreviousStack(github, previous, base, remote, reporter, error);
|
|
773
|
-
}
|
|
774
|
-
} else if (transition.kind === "append") {
|
|
775
|
-
reporter.progress(`Appending ${transition.branches.length} pull request${transition.branches.length === 1 ? "" : "s"} to stack #${transition.stackNumber}`);
|
|
776
|
-
github.appendToStack(transition.stackNumber, transition.branches.map((branch) => {
|
|
777
|
-
const index = changes.findIndex((change) => change.remoteBranch === branch);
|
|
778
|
-
const pullRequest = pullRequests[index];
|
|
779
|
-
if (!pullRequest) throw new Error(`Missing pull request for ${branch}`);
|
|
780
|
-
return pullRequest.number;
|
|
781
|
-
}), remote, options.draft);
|
|
782
|
-
} else if (transition.kind === "partial") reporter.progress("Updating this down-stack prefix while preserving higher pull requests");
|
|
783
|
-
else reporter.progress("The native GitHub stack already has the correct members");
|
|
855
|
+
applyStackTransition(transition, github, previous, changes, pullRequests, base, remote, options.draft, reporter);
|
|
784
856
|
const currentIds = new Set(changes.map((change) => change.id));
|
|
785
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");
|
|
786
858
|
if (omittedPullRequests.length > 0) {
|
|
@@ -807,15 +879,25 @@ function syncStack(dependencies, options) {
|
|
|
807
879
|
if (stackNumber !== void 0) updatedStack.stackNumber = stackNumber;
|
|
808
880
|
writeUpdatedState(stateStore, state, previous, updatedStack);
|
|
809
881
|
reporter.progress("Saved the local stack state");
|
|
882
|
+
const synchronized = changes.map((change, index) => ({
|
|
883
|
+
...change,
|
|
884
|
+
pullRequest: pullRequests[index]
|
|
885
|
+
}));
|
|
886
|
+
const outcomes = synchronized.map((change, index) => ({
|
|
887
|
+
outcome: changeOutcome(change, index, change.pullRequest, previous, changes, base, createdBranches, pushedBranches),
|
|
888
|
+
change,
|
|
889
|
+
pullRequest: change.pullRequest
|
|
890
|
+
}));
|
|
891
|
+
outcomes.push(...omittedPullRequests.map((pullRequest) => ({
|
|
892
|
+
outcome: "closed",
|
|
893
|
+
pullRequest
|
|
894
|
+
})));
|
|
810
895
|
return {
|
|
811
896
|
base,
|
|
812
897
|
remote,
|
|
813
898
|
rewritten,
|
|
814
|
-
changes:
|
|
815
|
-
|
|
816
|
-
pullRequest: pullRequests[index],
|
|
817
|
-
outcome: changeOutcome(change, index, pullRequests[index], previous, changes, base, createdBranches, pushedBranches)
|
|
818
|
-
}))
|
|
899
|
+
changes: synchronized,
|
|
900
|
+
outcomes
|
|
819
901
|
};
|
|
820
902
|
}
|
|
821
903
|
function changeOutcome(change, index, pullRequest, previous, changes, base, createdBranches, pushedBranches) {
|
|
@@ -826,16 +908,12 @@ function changeOutcome(change, index, pullRequest, previous, changes, base, crea
|
|
|
826
908
|
const metadataChanged = pullRequest.title !== change.subject || pullRequest.body !== change.body;
|
|
827
909
|
return pushedBranches.has(change.remoteBranch) || previousBase !== currentBase || metadataChanged ? "updated" : "unchanged";
|
|
828
910
|
}
|
|
829
|
-
function
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
github.linkStack(previous.changes.map((change) => change.pullRequest), base, remote, true);
|
|
834
|
-
} catch (rollbackError) {
|
|
835
|
-
const rollbackMessage = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
|
|
836
|
-
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;
|
|
837
915
|
}
|
|
838
|
-
|
|
916
|
+
reporter.progress(`Updating ${result.updated.length} of ${result.checked} remote branch${result.checked === 1 ? "" : "es"}`);
|
|
839
917
|
}
|
|
840
918
|
function writeUpdatedState(store, state, previous, updated) {
|
|
841
919
|
const stacks = previous ? state.stacks.map((stack) => stack === previous ? updated : stack) : [...state.stacks, updated];
|
|
@@ -939,13 +1017,7 @@ function main() {
|
|
|
939
1017
|
draft: values.draft,
|
|
940
1018
|
dryRun: values["dry-run"]
|
|
941
1019
|
});
|
|
942
|
-
|
|
943
|
-
console.log(values["dry-run"] ? `Would sync ${changeCount} against ${result.base}:` : `Synced stack against ${result.base} (${changeCount}):`);
|
|
944
|
-
for (const change of result.changes) {
|
|
945
|
-
const outcome = change.outcome ? `${change.outcome.padEnd(9)} ` : "";
|
|
946
|
-
const destination = change.pullRequest ? ` ${change.pullRequest.url}` : "";
|
|
947
|
-
console.log(` ${outcome}${change.oid.slice(0, 8)} ${change.subject}${destination}`);
|
|
948
|
-
}
|
|
1020
|
+
console.log(formatSyncResult(result, values["dry-run"]));
|
|
949
1021
|
}
|
|
950
1022
|
try {
|
|
951
1023
|
main();
|
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",
|