bstack 1.0.0 → 1.0.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # bstack
2
2
 
3
- Convert series of commits in a local branch into native GitHub stacked pull requests.
3
+ Convert a series of commits in a local branch into a native GitHub stack of pull requests.
4
4
 
5
5
  ## Install
6
6
 
@@ -17,7 +17,7 @@ gh extension install github/gh-stack
17
17
 
18
18
  ## Use
19
19
 
20
- Create one commit per change, then publish the stack:
20
+ Create one commit per change, then sync the stack:
21
21
 
22
22
  ```bash
23
23
  git switch -c my-feature
@@ -37,7 +37,7 @@ bstack checkout 123
37
37
  bstack checkout https://github.com/owner/repo/pull/123
38
38
  ```
39
39
 
40
- Use `--dry-run` to inspect without publishing and `--quiet` to hide progress logs.
40
+ Use `--dry-run` to inspect without syncing and `--quiet` to hide progress logs.
41
41
 
42
42
  ## References
43
43
 
package/dist/bstack.js CHANGED
@@ -6,7 +6,7 @@ import { z } from "zod";
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.0.0";
9
+ var version = "1.0.1";
10
10
  //#endregion
11
11
  //#region src/command.ts
12
12
  var CommandError = class extends Error {
@@ -75,24 +75,26 @@ function checkoutStack(repository, github, options) {
75
75
  }
76
76
  reporter.progress(`Checking out ${remote}/${headRef} in detached HEAD state`);
77
77
  repository.checkout(target);
78
- reporter.progress("Checkout complete; amend the commits and run bstack to publish updates");
78
+ reporter.progress("Checkout complete; amend the commits and run bstack to sync updates");
79
79
  return {
80
80
  headRef,
81
81
  delegated: false
82
82
  };
83
83
  }
84
84
  //#endregion
85
- //#region src/identity.ts
86
- const trailerPattern = /^bstack-id:\s*(\S+)\s*$/gm;
85
+ //#region src/commit.ts
86
+ const CHANGE_ID_TRAILER = "bstack-id";
87
+ const changeIdTrailerPattern = new RegExp(`^${CHANGE_ID_TRAILER}:\\s*(\\S+)\\s*$`, "gm");
88
+ const changeIdTrailerLinePattern = new RegExp(`^${CHANGE_ID_TRAILER}:\\s*\\S+\\s*$`);
87
89
  function readChangeId(message) {
88
- const matches = [...message.matchAll(trailerPattern)];
89
- if (matches.length > 1) throw new Error("A commit contains more than one bstack-id trailer");
90
+ const matches = [...message.matchAll(changeIdTrailerPattern)];
91
+ if (matches.length > 1) throw new Error(`A commit contains more than one ${CHANGE_ID_TRAILER} trailer`);
90
92
  return matches[0]?.[1];
91
93
  }
92
94
  function addChangeId(message, changeId) {
93
- return `${message.trimEnd()}\n\nbstack-id: ${changeId}\n`;
95
+ return `${message.trimEnd()}\n\n${CHANGE_ID_TRAILER}: ${changeId}\n`;
94
96
  }
95
- function newChangeId() {
97
+ function generateChangeId() {
96
98
  return randomUUID().replaceAll("-", "");
97
99
  }
98
100
  function parseRawCommit(oid, raw) {
@@ -129,7 +131,7 @@ function rewriteCommit(commit, parent, message) {
129
131
  return `${rewrittenHeaders.join("\n")}\n\n${message}`;
130
132
  }
131
133
  function splitCommitMessage(message) {
132
- const [subject = "Untitled change", ...bodyLines] = message.split("\n").filter((line) => !/^bstack-id:\s*\S+\s*$/.test(line)).join("\n").trim().split("\n");
134
+ const [subject = "Untitled change", ...bodyLines] = message.split("\n").filter((line) => !changeIdTrailerLinePattern.test(line)).join("\n").trim().split("\n");
133
135
  return {
134
136
  subject,
135
137
  body: bodyLines.join("\n").trim()
@@ -152,7 +154,7 @@ var GitRepository = class {
152
154
  }
153
155
  assertReady() {
154
156
  this.git(["rev-parse", "--show-toplevel"]);
155
- if (this.git(["status", "--porcelain"]).stdout.trim()) throw new Error("The working tree must be clean before bstack rewrites or publishes commits");
157
+ if (this.git(["status", "--porcelain"]).stdout.trim()) throw new Error("The working tree must be clean before bstack rewrites commits or pushes branches");
156
158
  }
157
159
  currentBranch() {
158
160
  return this.git([
@@ -232,7 +234,7 @@ var GitRepository = class {
232
234
  ]).stdout));
233
235
  }
234
236
  ensureChangeIds(commits, dryRun, userLogin) {
235
- const assigned = commits.map((commit) => commit.changeId ?? newChangeId());
237
+ const assigned = commits.map((commit) => commit.changeId ?? generateChangeId());
236
238
  const needsRewrite = commits.some((commit) => commit.changeId === void 0);
237
239
  let parent = commits[0]?.parent;
238
240
  const rewrittenOids = [];
@@ -327,7 +329,7 @@ const pullRequestSchema = z.object({
327
329
  isDraft: z.boolean()
328
330
  });
329
331
  const stackSchema = z.object({ number: z.number() });
330
- var GhPlatform = class {
332
+ var GitHubCliPlatform = class {
331
333
  cwd;
332
334
  runner;
333
335
  constructor(cwd, runner) {
@@ -588,19 +590,19 @@ function syncStack(repository, github, options) {
588
590
  const store = new StateStore(repository.statePath());
589
591
  const state = store.read();
590
592
  const previous = store.findByChangeIds(state, new Set(changes.map((change) => change.id)));
591
- const evolution = analyzeEvolution(previous, changes, github, repository.currentBranch() === "");
592
- reporter.progress(`Publishing ${changes.length} protected remote ref${changes.length === 1 ? "" : "s"}`);
593
+ const transition = analyzeStackTransition(previous, changes, github, repository.currentBranch() === "");
594
+ reporter.progress(`Pushing ${changes.length} remote branch${changes.length === 1 ? "" : "es"}`);
593
595
  repository.pushChanges(remote, changes);
594
596
  reporter.progress("Looking up existing pull requests");
595
597
  const existing = changes.map((change) => github.pullRequestForBranch(change.remoteBranch));
596
598
  let pullRequests;
597
599
  if (changes.length === 1) {
598
- if (evolution.kind === "partial") reporter.progress("Updating this down-stack prefix while preserving higher pull requests");
600
+ if (transition.kind === "partial") reporter.progress("Updating this down-stack prefix while preserving higher pull requests");
599
601
  reporter.progress(existing[0] ? "Using the existing pull request" : "Creating a pull request");
600
602
  const pullRequest = existing[0] ?? github.createPullRequest(changes[0], base, options.draft);
601
- if (evolution.kind === "collapse") {
602
- reporter.progress(`Removing omitted pull requests from stack #${evolution.stackNumber}`);
603
- github.unstack(evolution.stackNumber);
603
+ if (transition.kind === "collapse") {
604
+ reporter.progress(`Removing omitted pull requests from stack #${transition.stackNumber}`);
605
+ github.unstack(transition.stackNumber);
604
606
  try {
605
607
  github.editPullRequestBase(pullRequest, base);
606
608
  } catch (error) {
@@ -609,21 +611,21 @@ function syncStack(repository, github, options) {
609
611
  }
610
612
  pullRequests = [pullRequest];
611
613
  } else {
612
- if (evolution.kind === "full") {
614
+ if (transition.kind === "full") {
613
615
  reporter.progress(`Linking ${changes.length} pull requests as a native GitHub stack`);
614
616
  github.linkStack(changes.map((change) => change.remoteBranch), base, remote, options.draft);
615
- } else if (evolution.kind === "rebuild") {
616
- reporter.progress(`Rebuilding stack #${evolution.stackNumber} to ${evolution.action} pull requests`);
617
- github.unstack(evolution.stackNumber);
617
+ } else if (transition.kind === "rebuild") {
618
+ reporter.progress(`Rebuilding stack #${transition.stackNumber} to ${transition.action} pull requests`);
619
+ github.unstack(transition.stackNumber);
618
620
  try {
619
621
  github.linkStack(changes.map((change) => change.remoteBranch), base, remote, options.draft);
620
622
  } catch (error) {
621
623
  restorePreviousStack(github, previous, base, remote, reporter, error);
622
624
  }
623
- } else if (evolution.kind === "append") {
624
- reporter.progress(`Appending ${evolution.branches.length} pull request${evolution.branches.length === 1 ? "" : "s"} to stack #${evolution.stackNumber}`);
625
- github.appendToStack(evolution.stackNumber, evolution.branches, remote, options.draft);
626
- } else if (evolution.kind === "partial") reporter.progress("Updating this down-stack prefix while preserving higher pull requests");
625
+ } else if (transition.kind === "append") {
626
+ reporter.progress(`Appending ${transition.branches.length} pull request${transition.branches.length === 1 ? "" : "s"} to stack #${transition.stackNumber}`);
627
+ github.appendToStack(transition.stackNumber, transition.branches, remote, options.draft);
628
+ } else if (transition.kind === "partial") reporter.progress("Updating this down-stack prefix while preserving higher pull requests");
627
629
  else reporter.progress("The native GitHub stack already has the correct members");
628
630
  pullRequests = changes.map((change, index) => {
629
631
  const pr = existing[index] ?? github.pullRequestForBranch(change.remoteBranch);
@@ -636,20 +638,20 @@ function syncStack(repository, github, options) {
636
638
  github.editPullRequest(pr, changes[index]);
637
639
  reporter.progress(`PR #${pr.number}: ${changes[index].subject}`);
638
640
  }
639
- const stackNumber = evolution.kind === "rebuild" ? github.stackNumberForPullRequest(pullRequests[0].number) : evolution.kind === "collapse" ? void 0 : previous?.stackNumber ?? (pullRequests.length > 1 ? github.stackNumberForPullRequest(pullRequests[0].number) : void 0);
641
+ const stackNumber = transition.kind === "rebuild" ? github.stackNumberForPullRequest(pullRequests[0].number) : transition.kind === "collapse" ? void 0 : previous?.stackNumber ?? (pullRequests.length > 1 ? github.stackNumberForPullRequest(pullRequests[0].number) : void 0);
640
642
  const synchronizedChanges = changes.map((change, index) => ({
641
643
  id: change.id,
642
644
  remoteBranch: change.remoteBranch,
643
645
  pullRequest: pullRequests[index].number,
644
646
  url: pullRequests[index].url
645
647
  }));
646
- const stored = {
648
+ const updatedStack = {
647
649
  remote,
648
650
  base,
649
- changes: evolution.kind === "partial" && previous ? [...synchronizedChanges, ...previous.changes.slice(evolution.previousOffset + changes.length)] : synchronizedChanges
651
+ changes: transition.kind === "partial" && previous ? [...synchronizedChanges, ...previous.changes.slice(transition.previousOffset + changes.length)] : synchronizedChanges
650
652
  };
651
- if (stackNumber !== void 0) stored.stackNumber = stackNumber;
652
- writeUpdatedState(store, state, previous, stored);
653
+ if (stackNumber !== void 0) updatedStack.stackNumber = stackNumber;
654
+ writeUpdatedState(store, state, previous, updatedStack);
653
655
  reporter.progress("Saved the local stack state");
654
656
  return {
655
657
  base,
@@ -661,13 +663,13 @@ function syncStack(repository, github, options) {
661
663
  }))
662
664
  };
663
665
  }
664
- function analyzeEvolution(previous, changes, github, preserveHigherChanges) {
666
+ function analyzeStackTransition(previous, changes, github, preserveHigherChanges) {
665
667
  if (!previous) return { kind: "full" };
666
668
  const previousIds = previous.changes.map((change) => change.id);
667
669
  const currentIds = changes.map((change) => change.id);
668
670
  const previousIdSet = new Set(previousIds);
669
671
  const currentIdSet = new Set(currentIds);
670
- if (!sameValues(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");
672
+ 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");
671
673
  const removed = previous.changes.filter((change) => !currentIdSet.has(change.id));
672
674
  const added = changes.filter((change) => !previousIdSet.has(change.id));
673
675
  if (removed.length === 0) {
@@ -711,7 +713,7 @@ function analyzeEvolution(previous, changes, github, preserveHigherChanges) {
711
713
  action: added.length === 0 ? "remove" : "update"
712
714
  };
713
715
  }
714
- function sameValues(left, right) {
716
+ function sameSequence(left, right) {
715
717
  return left.length === right.length && left.every((value, index) => value === right[index]);
716
718
  }
717
719
  function restorePreviousStack(github, previous, base, remote, reporter, rebuildError) {
@@ -734,14 +736,14 @@ function writeUpdatedState(store, state, previous, updated) {
734
736
  }
735
737
  //#endregion
736
738
  //#region src/cli.ts
737
- const help = `bstack - turn a linear commit series into native GitHub stacked PRs
739
+ const help = `bstack - turn a linear commit series into a native GitHub stack of PRs
738
740
 
739
741
  Usage:
740
742
  bstack [sync] [options]
741
743
  bstack checkout <PR-number-or-URL> [options]
742
744
 
743
745
  Options:
744
- --base <branch> Stack trunk; defaults to the GitHub default branch
746
+ --base <branch> Stack base; defaults to the GitHub default branch
745
747
  --remote <name> Git remote; defaults to remote.pushDefault or origin
746
748
  --draft Create draft PRs instead of ready-for-review PRs
747
749
  --dry-run Inspect the stack without rewriting commits or pushing
@@ -796,7 +798,7 @@ function main() {
796
798
  const runner = new NodeCommandRunner();
797
799
  const cwd = process.cwd();
798
800
  const repository = new GitRepository(cwd, runner);
799
- const github = new GhPlatform(cwd, runner);
801
+ const github = new GitHubCliPlatform(cwd, runner);
800
802
  const reporter = new ConsoleReporter(!values.quiet);
801
803
  const command = positionals[0] ?? "sync";
802
804
  if (command === "checkout") {
@@ -820,7 +822,7 @@ function main() {
820
822
  dryRun: values["dry-run"],
821
823
  reporter
822
824
  });
823
- console.log(`${values["dry-run"] ? "Would publish" : "Published"} ${result.changes.length} change${result.changes.length === 1 ? "" : "s"} against ${result.base}:`);
825
+ console.log(`${values["dry-run"] ? "Would sync" : "Synced"} ${result.changes.length} change${result.changes.length === 1 ? "" : "s"} against ${result.base}:`);
824
826
  for (const change of result.changes) {
825
827
  const destination = change.pullRequest ? ` ${change.pullRequest.url}` : "";
826
828
  console.log(` ${change.oid.slice(0, 8)} ${change.subject}${destination}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bstack",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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": {