gh-stack 0.1.0 → 0.1.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
@@ -47,23 +47,24 @@ gh-stack status
47
47
  ### Stack Management
48
48
 
49
49
  ```
50
- init [--name <name>] [--description <desc>]
50
+ init [--name <name>] [--description <desc>] [--parent <branch>]
51
51
  Create a new stack and add the current branch as the first entry.
52
52
 
53
- add [<branch>] [--parent <branch>] [--create] [--description <desc>]
53
+ add [<branch>] [--parent <branch>] [--create <branch>] [--description <desc>]
54
54
  Add a branch to the current stack. Defaults to the current branch.
55
- --create makes a new branch off the top of stack first.
55
+ --create <branch> makes a new branch off the top of stack first.
56
56
 
57
- remove <branch>
57
+ remove [<branch>]
58
58
  Remove a branch from the stack and re-parent its children.
59
+ If omitted, an interactive selector is shown.
59
60
  ```
60
61
 
61
62
  ### Navigation
62
63
 
63
64
  ```
64
65
  show
65
- Display the current stack as a tree with PR numbers, titles,
66
- review status, and CI state. This is the default command.
66
+ Display the current stack as a tree with branch numbers,
67
+ PR numbers, and descriptions. This is the default command.
67
68
 
68
69
  list
69
70
  List branches with position numbers. Lightweight output for
@@ -104,6 +105,7 @@ merge [--dry-run]
104
105
  Squash-merge the stack top-down locally (PR3 -> PR2 -> PR1),
105
106
  then optionally rebase onto main. Keeps all commits local to
106
107
  avoid orphaned squash commits from GitHub-only merges.
108
+ Can also close intermediate PRs and archives the stack on completion.
107
109
 
108
110
  update-prs
109
111
  Update all PR descriptions with a stack visualization:
@@ -119,13 +121,25 @@ update-prs
119
121
  ### Maintenance
120
122
 
121
123
  ```
122
- archive [--list] [--restore <name>]
123
- Manage archived stacks.
124
+ archive [--restore <name>]
125
+ List archived stacks by default, or restore one by name.
124
126
 
125
127
  undo
126
128
  Restore the last snapshot taken before a destructive operation.
127
129
  ```
128
130
 
131
+ ## Not Implemented Yet
132
+
133
+ These are still intended, but not shipped in `v0.1.1`:
134
+
135
+ ```text
136
+ split
137
+ Guided helper for splitting a branch/PR into two and updating stack metadata.
138
+
139
+ insert
140
+ Insert a branch into the middle of an existing stack and re-parent the chain.
141
+ ```
142
+
129
143
  ## Global Options
130
144
 
131
145
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gh-stack",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Stacked PR manager for squash-merge workflows",
5
5
  "author": "Kiliman <kiliman@gmail.com>",
6
6
  "license": "MIT",
@@ -5,7 +5,7 @@ import * as git from "../lib/git.ts";
5
5
  import { addBranchToStack, getTopOfStack, getOrderedBranches } from "../lib/metadata.ts";
6
6
  import { ensureMetadata, ensureCurrentStack } from "../lib/safety.ts";
7
7
  import { getPrNumber } from "../lib/github.ts";
8
- import { selectParent } from "../lib/ui.ts";
8
+ import { isAutoYes, selectParent } from "../lib/ui.ts";
9
9
  import type { Branch } from "../types.ts";
10
10
 
11
11
  export default async function add(args: string[]): Promise<void> {
@@ -95,6 +95,8 @@ OPTIONS
95
95
 
96
96
  if (ordered.length === 0) {
97
97
  parent = "main";
98
+ } else if (isAutoYes()) {
99
+ parent = getTopOfStack(stack) ?? "main";
98
100
  } else {
99
101
  parent = (await selectParent(stack, branchToAdd)) ?? undefined;
100
102
  if (!parent) {
@@ -26,7 +26,8 @@ ALIASES
26
26
  const currentBranch = await git.currentBranch();
27
27
 
28
28
  // Find which stack contains the current branch
29
- let stackName = findStackForBranch(meta, currentBranch);
29
+ const branchStackName = findStackForBranch(meta, currentBranch);
30
+ let stackName = branchStackName;
30
31
  if (!stackName) stackName = meta.current_stack;
31
32
 
32
33
  if (!stackName || !meta.stacks[stackName]) {
@@ -39,7 +40,9 @@ ALIASES
39
40
 
40
41
  // Update tracking
41
42
  meta.current_stack = stackName;
42
- stack.last_branch = currentBranch;
43
+ if (branchStackName === stackName) {
44
+ stack.last_branch = currentBranch;
45
+ }
43
46
  await writeMetadata(meta);
44
47
 
45
48
  // Header
@@ -235,25 +235,27 @@ async function handleFreshRestack(
235
235
  console.log();
236
236
  }
237
237
 
238
- // Take snapshot before destructive operation
239
- await takeSnapshot(meta, stackName, "restack");
238
+ if (!dryRun) {
239
+ // Take snapshot before destructive operation
240
+ await takeSnapshot(meta, stackName, "restack");
240
241
 
241
- // Create temporary tags for stable base references
242
- p.log.info("Creating temporary base tags...");
243
- for (const branch of chain) {
244
- const parent = stack.branches[branch]?.parent;
245
- if (!parent) continue;
242
+ // Create temporary tags for stable base references
243
+ p.log.info("Creating temporary base tags...");
244
+ for (const branch of chain) {
245
+ const parent = stack.branches[branch]?.parent;
246
+ if (!parent) continue;
246
247
 
247
- const mb = await git.mergeBase(branch, parent);
248
- if (mb) {
249
- const tagName = `stack-sync-base-${git.sanitizeBranchForTag(branch)}`;
250
- await git.createTag(tagName, mb);
251
- console.log(
252
- ` ${pc.green("✓")} Tagged base for ${branch}: ${pc.cyan(tagName)} (${mb.slice(0, 8)})`,
253
- );
248
+ const mb = await git.mergeBase(branch, parent);
249
+ if (mb) {
250
+ const tagName = `stack-sync-base-${git.sanitizeBranchForTag(branch)}`;
251
+ await git.createTag(tagName, mb);
252
+ console.log(
253
+ ` ${pc.green("✓")} Tagged base for ${branch}: ${pc.cyan(tagName)} (${mb.slice(0, 8)})`,
254
+ );
255
+ }
254
256
  }
257
+ console.log();
255
258
  }
256
- console.log();
257
259
 
258
260
  // Process the chain
259
261
  await processChain(meta, stackName, chain, 0, rebasedBranches, dryRun, verbose);
@@ -29,7 +29,8 @@ export default async function show(_args: string[]): Promise<void> {
29
29
  const branch = await git.currentBranch();
30
30
 
31
31
  // Find which stack contains the current branch
32
- let stackName = findStackForBranch(meta, branch);
32
+ const branchStackName = findStackForBranch(meta, branch);
33
+ let stackName = branchStackName;
33
34
 
34
35
  if (!stackName) {
35
36
  // Fall back to current_stack
@@ -51,7 +52,9 @@ export default async function show(_args: string[]): Promise<void> {
51
52
 
52
53
  // Update current_stack and last_branch
53
54
  meta.current_stack = stackName;
54
- stack.last_branch = branch;
55
+ if (branchStackName === stackName) {
56
+ stack.last_branch = branch;
57
+ }
55
58
  await writeMetadata(meta);
56
59
 
57
60
  // Header
@@ -54,6 +54,28 @@ with the base branch included.
54
54
  process.exit(1);
55
55
  }
56
56
 
57
+ if (dryRun) {
58
+ p.log.warn("[DRY RUN] No changes will be made");
59
+ p.log.info("Planned operations:");
60
+ console.log(` ${pc.yellow("1.")} Fetch latest ${pc.cyan("origin/main")}`);
61
+ console.log(
62
+ ` ${pc.yellow("2.")} Rebase ${pc.yellow(baseBranch)} onto ${pc.cyan("origin/main")}`,
63
+ );
64
+
65
+ const children = ordered.slice(1);
66
+ if (children.length === 0) {
67
+ console.log(` ${pc.yellow("3.")} No child branches to restack`);
68
+ } else {
69
+ console.log(` ${pc.yellow("3.")} Restack child branches in order:`);
70
+ for (const branch of children) {
71
+ const parent = stack.branches[branch]?.parent || "(unknown)";
72
+ console.log(` ${pc.yellow("→")} ${branch} ${pc.blue(`(parent: ${parent})`)}`);
73
+ }
74
+ }
75
+ p.outro(pc.green("Sync dry run complete"));
76
+ return;
77
+ }
78
+
57
79
  // Fetch main
58
80
  const fetchSpinner = p.spinner();
59
81
  fetchSpinner.start("Fetching latest main...");
@@ -46,6 +46,7 @@ showing the tree structure, PR links, and review status.
46
46
  branch: string;
47
47
  prNumber: number | null;
48
48
  prTitle: string;
49
+ prUrl: string | null;
49
50
  reviewEmojiStr: string;
50
51
  }
51
52
 
@@ -55,12 +56,14 @@ showing the tree structure, PR links, and review status.
55
56
  const branchMeta = stack.branches[branchName]!;
56
57
  const prNumber = branchMeta.pr ?? null;
57
58
  let prTitle = branchMeta.description || branchName;
59
+ let prUrl: string | null = null;
58
60
  let review = "PENDING";
59
61
 
60
62
  if (prNumber) {
61
63
  const info = await getPrInfo(prNumber);
62
64
  if (info) {
63
65
  prTitle = info.title || prTitle;
66
+ prUrl = info.url;
64
67
  review = info.reviewDecision || "PENDING";
65
68
  }
66
69
  }
@@ -69,6 +72,7 @@ showing the tree structure, PR links, and review status.
69
72
  branch: branchName,
70
73
  prNumber,
71
74
  prTitle,
75
+ prUrl,
72
76
  reviewEmojiStr: reviewEmoji(review),
73
77
  });
74
78
  }
@@ -131,10 +135,11 @@ interface BranchPrInfo {
131
135
  branch: string;
132
136
  prNumber: number | null;
133
137
  prTitle: string;
138
+ prUrl: string | null;
134
139
  reviewEmojiStr: string;
135
140
  }
136
141
 
137
- function buildStackViz(branches: BranchPrInfo[], targetIndex: number): string {
142
+ export function buildStackViz(branches: BranchPrInfo[], targetIndex: number): string {
138
143
  const lines: string[] = ["### 📚 Stacked on", ""];
139
144
 
140
145
  if (branches.length === 1) {
@@ -154,7 +159,7 @@ function buildStackViz(branches: BranchPrInfo[], targetIndex: number): string {
154
159
  // PR link
155
160
  let prLink: string;
156
161
  if (info.prNumber) {
157
- prLink = `<a href="https://github.com/beehiiv/swarm/pull/${info.prNumber}">#${info.prNumber}</a>`;
162
+ prLink = info.prUrl ? `<a href="${info.prUrl}">#${info.prNumber}</a>` : `#${info.prNumber}`;
158
163
  } else {
159
164
  prLink = "(no PR yet)";
160
165
  }