bstack 0.2.1 → 0.3.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 +14 -26
- package/dist/bstack.js +46 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
# bstack
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Convert series of commits in a local branch into native GitHub stacked pull requests.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g bstack
|
|
9
|
+
```
|
|
8
10
|
|
|
9
|
-
-
|
|
10
|
-
- `gh cli` plus the `github/gh-stack` extension (authenticated)
|
|
11
|
+
Install and authenticate the GitHub CLI with the `github/gh-stack` extension:
|
|
11
12
|
|
|
12
|
-
```
|
|
13
|
+
```
|
|
14
|
+
gh auth login
|
|
13
15
|
gh extension install github/gh-stack
|
|
14
|
-
npm install bstack -g
|
|
15
16
|
```
|
|
16
17
|
|
|
17
18
|
## Use
|
|
18
19
|
|
|
19
|
-
Create one commit per
|
|
20
|
+
Create one commit per change, then publish the stack:
|
|
20
21
|
|
|
21
22
|
```bash
|
|
22
23
|
git switch -c my-feature
|
|
@@ -25,7 +26,9 @@ git commit -am "feat: add the API"
|
|
|
25
26
|
bstack
|
|
26
27
|
```
|
|
27
28
|
|
|
28
|
-
Run `bstack` again after amending or rebasing commits.
|
|
29
|
+
Run `bstack` again after amending or rebasing commits.
|
|
30
|
+
|
|
31
|
+
New PRs are ready for review by default. Pass `--draft` to create draft PRs.
|
|
29
32
|
|
|
30
33
|
Checkout a stack through one of its PRs:
|
|
31
34
|
|
|
@@ -34,24 +37,9 @@ bstack checkout 123
|
|
|
34
37
|
bstack checkout https://github.com/owner/repo/pull/123
|
|
35
38
|
```
|
|
36
39
|
|
|
37
|
-
The first publish adds a stable `Bstack-Id` trailer to each commit and rewrites their hashes. The working tree must be clean. Merge commits and signed commits that need trailers are not supported.
|
|
38
|
-
|
|
39
40
|
Use `--dry-run` to inspect without publishing and `--quiet` to hide progress logs.
|
|
40
41
|
|
|
41
|
-
##
|
|
42
|
-
|
|
43
|
-
Install dependencies and run the checks with pnpm:
|
|
42
|
+
## References
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
pnpm run type-check
|
|
48
|
-
pnpm test
|
|
49
|
-
pnpm run lint
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Build the Node.js CLI with tsdown:
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
pnpm run build
|
|
56
|
-
node dist/bstack.js --help
|
|
57
|
-
```
|
|
44
|
+
- [ezyang/ghstack](https://github.com/ezyang/ghstack)
|
|
45
|
+
- [github/gh-stack](https://github.com/github/gh-stack)
|
package/dist/bstack.js
CHANGED
|
@@ -5,7 +5,7 @@ import { randomUUID } from "node:crypto";
|
|
|
5
5
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname } from "node:path";
|
|
7
7
|
//#region package.json
|
|
8
|
-
var version = "0.
|
|
8
|
+
var version = "0.3.0";
|
|
9
9
|
//#endregion
|
|
10
10
|
//#region src/command.ts
|
|
11
11
|
var CommandError = class extends Error {
|
|
@@ -368,7 +368,7 @@ var GhPlatform = class {
|
|
|
368
368
|
]).stdout;
|
|
369
369
|
return normalizePullRequest(JSON.parse(raw));
|
|
370
370
|
}
|
|
371
|
-
createPullRequest(change, base,
|
|
371
|
+
createPullRequest(change, base, draft) {
|
|
372
372
|
const args = [
|
|
373
373
|
"pr",
|
|
374
374
|
"create",
|
|
@@ -381,13 +381,13 @@ var GhPlatform = class {
|
|
|
381
381
|
"--body",
|
|
382
382
|
change.body
|
|
383
383
|
];
|
|
384
|
-
if (
|
|
384
|
+
if (draft) args.push("--draft");
|
|
385
385
|
this.gh(args);
|
|
386
386
|
const created = this.pullRequestForBranch(change.remoteBranch);
|
|
387
387
|
if (!created) throw new Error(`GitHub did not return the PR created for ${change.remoteBranch}`);
|
|
388
388
|
return created;
|
|
389
389
|
}
|
|
390
|
-
linkStack(branches, base, remote,
|
|
390
|
+
linkStack(branches, base, remote, draft) {
|
|
391
391
|
const args = [
|
|
392
392
|
"stack",
|
|
393
393
|
"link",
|
|
@@ -396,21 +396,28 @@ var GhPlatform = class {
|
|
|
396
396
|
"--remote",
|
|
397
397
|
remote
|
|
398
398
|
];
|
|
399
|
-
if (
|
|
399
|
+
if (!draft) args.push("--open");
|
|
400
400
|
args.push(...branches);
|
|
401
401
|
this.gh(args);
|
|
402
402
|
}
|
|
403
|
-
appendToStack(stackNumber, branches, remote,
|
|
403
|
+
appendToStack(stackNumber, branches, remote, draft) {
|
|
404
404
|
const args = [
|
|
405
405
|
"stack",
|
|
406
406
|
"link",
|
|
407
407
|
"--remote",
|
|
408
408
|
remote
|
|
409
409
|
];
|
|
410
|
-
if (
|
|
410
|
+
if (!draft) args.push("--open");
|
|
411
411
|
args.push(String(stackNumber), ...branches);
|
|
412
412
|
this.gh(args);
|
|
413
413
|
}
|
|
414
|
+
unstack(stackNumber) {
|
|
415
|
+
this.gh([
|
|
416
|
+
"stack",
|
|
417
|
+
"unstack",
|
|
418
|
+
String(stackNumber)
|
|
419
|
+
]);
|
|
420
|
+
}
|
|
414
421
|
editPullRequest(pr, change) {
|
|
415
422
|
if (pr.title === change.subject && pr.body === change.body) return;
|
|
416
423
|
this.gh([
|
|
@@ -547,14 +554,28 @@ function syncStack(repository, github, options) {
|
|
|
547
554
|
if (changes.length === 1) {
|
|
548
555
|
if (evolution.kind === "partial") reporter.progress("Updating this down-stack prefix while preserving higher pull requests");
|
|
549
556
|
reporter.progress(existing[0] ? "Using the existing pull request" : "Creating a pull request");
|
|
550
|
-
pullRequests = [existing[0] ?? github.createPullRequest(changes[0], base, options.
|
|
557
|
+
pullRequests = [existing[0] ?? github.createPullRequest(changes[0], base, options.draft)];
|
|
551
558
|
} else {
|
|
552
559
|
if (evolution.kind === "full") {
|
|
553
560
|
reporter.progress(`Linking ${changes.length} pull requests as a native GitHub stack`);
|
|
554
|
-
github.linkStack(changes.map((change) => change.remoteBranch), base, remote, options.
|
|
561
|
+
github.linkStack(changes.map((change) => change.remoteBranch), base, remote, options.draft);
|
|
562
|
+
} else if (evolution.kind === "rebuild") {
|
|
563
|
+
reporter.progress(`Rebuilding stack #${evolution.stackNumber} to insert pull requests`);
|
|
564
|
+
github.unstack(evolution.stackNumber);
|
|
565
|
+
try {
|
|
566
|
+
github.linkStack(changes.map((change) => change.remoteBranch), base, remote, options.draft);
|
|
567
|
+
} catch (error) {
|
|
568
|
+
reporter.progress("Rebuild failed; restoring the previous native GitHub stack");
|
|
569
|
+
try {
|
|
570
|
+
github.linkStack(previous.changes.map((change) => change.remoteBranch), base, remote, true);
|
|
571
|
+
} catch (rollbackError) {
|
|
572
|
+
throw new Error(`Stack rebuild failed: ${errorMessage(error)}\nRestoring the previous stack also failed: ${errorMessage(rollbackError)}`);
|
|
573
|
+
}
|
|
574
|
+
throw error;
|
|
575
|
+
}
|
|
555
576
|
} else if (evolution.kind === "append") {
|
|
556
577
|
reporter.progress(`Appending ${evolution.branches.length} pull request${evolution.branches.length === 1 ? "" : "s"} to stack #${evolution.stackNumber}`);
|
|
557
|
-
github.appendToStack(evolution.stackNumber, evolution.branches, remote, options.
|
|
578
|
+
github.appendToStack(evolution.stackNumber, evolution.branches, remote, options.draft);
|
|
558
579
|
} else if (evolution.kind === "partial") reporter.progress("Updating this down-stack prefix while preserving higher pull requests");
|
|
559
580
|
else reporter.progress("The native GitHub stack already has the correct members");
|
|
560
581
|
pullRequests = changes.map((change, index) => {
|
|
@@ -568,7 +589,7 @@ function syncStack(repository, github, options) {
|
|
|
568
589
|
github.editPullRequest(pr, changes[index]);
|
|
569
590
|
reporter.progress(`PR #${pr.number}: ${changes[index].subject}`);
|
|
570
591
|
}
|
|
571
|
-
const stackNumber = previous?.stackNumber ?? (pullRequests.length > 1 ? github.stackNumberForPullRequest(pullRequests[0].number) : void 0);
|
|
592
|
+
const stackNumber = evolution.kind === "rebuild" ? github.stackNumberForPullRequest(pullRequests[0].number) : previous?.stackNumber ?? (pullRequests.length > 1 ? github.stackNumberForPullRequest(pullRequests[0].number) : void 0);
|
|
572
593
|
const synchronizedChanges = changes.map((change, index) => ({
|
|
573
594
|
id: change.id,
|
|
574
595
|
remoteBranch: change.remoteBranch,
|
|
@@ -597,7 +618,14 @@ function analyzeEvolution(previous, changes, github) {
|
|
|
597
618
|
if (!previous) return { kind: "full" };
|
|
598
619
|
const previousIds = previous.changes.map((change) => change.id);
|
|
599
620
|
const currentIds = changes.map((change) => change.id);
|
|
600
|
-
if (isSubsequence(previousIds, currentIds))
|
|
621
|
+
if (isSubsequence(previousIds, currentIds)) {
|
|
622
|
+
if (previousIds.every((id, index) => currentIds[index] === id)) return { kind: "full" };
|
|
623
|
+
const stackNumber = previous.stackNumber ?? github.stackNumberForPullRequest(previous.changes[0].pullRequest);
|
|
624
|
+
return stackNumber === void 0 ? { kind: "full" } : {
|
|
625
|
+
kind: "rebuild",
|
|
626
|
+
stackNumber
|
|
627
|
+
};
|
|
628
|
+
}
|
|
601
629
|
const firstCurrentIndex = previousIds.indexOf(currentIds[0]);
|
|
602
630
|
if (firstCurrentIndex === -1) throw new Error("The current commits do not continue the previously submitted stack");
|
|
603
631
|
const removedPrefix = previous.changes.slice(0, firstCurrentIndex);
|
|
@@ -624,6 +652,9 @@ function isSubsequence(expected, actual) {
|
|
|
624
652
|
for (const value of actual) if (value === expected[expectedIndex]) expectedIndex++;
|
|
625
653
|
return expectedIndex === expected.length;
|
|
626
654
|
}
|
|
655
|
+
function errorMessage(error) {
|
|
656
|
+
return error instanceof Error ? error.message : String(error);
|
|
657
|
+
}
|
|
627
658
|
function writeUpdatedState(store, state, previous, updated) {
|
|
628
659
|
const stacks = previous ? state.stacks.map((stack) => stack === previous ? updated : stack) : [...state.stacks, updated];
|
|
629
660
|
store.write({
|
|
@@ -642,7 +673,7 @@ Usage:
|
|
|
642
673
|
Options:
|
|
643
674
|
--base <branch> Stack trunk; defaults to the GitHub default branch
|
|
644
675
|
--remote <name> Git remote; defaults to remote.pushDefault or origin
|
|
645
|
-
--
|
|
676
|
+
--draft Create draft PRs instead of ready-for-review PRs
|
|
646
677
|
--dry-run Inspect the stack without rewriting commits or pushing
|
|
647
678
|
--quiet Hide progress logs; the final summary is still printed
|
|
648
679
|
--same-base Refuse checkout if it would change the current merge base
|
|
@@ -656,7 +687,7 @@ function main() {
|
|
|
656
687
|
options: {
|
|
657
688
|
base: { type: "string" },
|
|
658
689
|
remote: { type: "string" },
|
|
659
|
-
|
|
690
|
+
draft: {
|
|
660
691
|
type: "boolean",
|
|
661
692
|
default: false
|
|
662
693
|
},
|
|
@@ -715,7 +746,7 @@ function main() {
|
|
|
715
746
|
const result = syncStack(repository, github, {
|
|
716
747
|
base: values.base,
|
|
717
748
|
remote: values.remote,
|
|
718
|
-
|
|
749
|
+
draft: values.draft,
|
|
719
750
|
dryRun: values["dry-run"],
|
|
720
751
|
reporter
|
|
721
752
|
});
|