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.
Files changed (3) hide show
  1. package/README.md +14 -26
  2. package/dist/bstack.js +46 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,22 +1,23 @@
1
1
  # bstack
2
2
 
3
- `bstack` turns a series of commits on a local branch into native GitHub stacked pull requests.
3
+ Convert series of commits in a local branch into native GitHub stacked pull requests.
4
4
 
5
5
  ## Install
6
6
 
7
- Requirements:
7
+ ```bash
8
+ npm install -g bstack
9
+ ```
8
10
 
9
- - Node.js 20+
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
- ```bash
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 reviewable change, then publish the stack:
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. New PRs are drafts unless you pass `--open`.
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
- ## Develop
42
-
43
- Install dependencies and run the checks with pnpm:
42
+ ## References
44
43
 
45
- ```bash
46
- pnpm install
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.2.1";
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, open) {
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 (!open) args.push("--draft");
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, open) {
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 (open) args.push("--open");
399
+ if (!draft) args.push("--open");
400
400
  args.push(...branches);
401
401
  this.gh(args);
402
402
  }
403
- appendToStack(stackNumber, branches, remote, open) {
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 (open) args.push("--open");
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.open)];
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.open);
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.open);
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)) return { kind: "full" };
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
- --open Create PRs ready for review instead of drafts
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
- open: {
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
- open: values.open,
749
+ draft: values.draft,
719
750
  dryRun: values["dry-run"],
720
751
  reporter
721
752
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bstack",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "packageManager": "pnpm@11.5.1",
5
5
  "description": "Create native GitHub stacked pull requests from a linear series of commits",
6
6
  "repository": {