gh-stack 0.1.1 → 0.2.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.
@@ -0,0 +1,310 @@
1
+ // gh-stack submit — Push branches and create/update PRs
2
+ import * as p from "@clack/prompts";
3
+ import pc from "picocolors";
4
+ import { $ } from "bun";
5
+ import * as git from "../lib/git.ts";
6
+ import { findStackForBranch, getOrderedBranches, writeMetadata } from "../lib/metadata.ts";
7
+ import { ensureMetadata } from "../lib/safety.ts";
8
+ import { isAutoYes } from "../lib/ui.ts";
9
+ import { getPrNumber, getPrInfo, getPrBody, updatePrBody, reviewEmoji } from "../lib/github.ts";
10
+ import { buildStackViz } from "./update-prs.ts";
11
+
12
+ const HELP = `
13
+ gh-stack submit — Push branches and create/update PRs
14
+
15
+ USAGE
16
+ gh-stack submit [options]
17
+
18
+ Pushes all branches from trunk to the current branch, creating PRs
19
+ for branches that don't have them and updating stack visualization
20
+ in all PR descriptions. Idempotent — safe to run repeatedly.
21
+
22
+ OPTIONS
23
+ -d, --draft Create new PRs as drafts
24
+ -n, --no-edit Don't prompt for PR titles (auto-generate from branch name)
25
+ --dry-run Show what would happen without doing anything
26
+
27
+ EXAMPLES
28
+ gh-stack submit # Push + create PRs interactively
29
+ gh-stack submit -n # Push + create PRs with auto-titles
30
+ gh-stack submit --draft # Create new PRs as drafts
31
+ gh-stack submit --dry-run # Preview what would happen
32
+ `;
33
+
34
+ /**
35
+ * Generate a PR title from a branch name.
36
+ * Strips common prefixes like "kiliman/", replaces hyphens with spaces,
37
+ * and capitalizes the first letter.
38
+ */
39
+ function branchToTitle(branch: string): string {
40
+ // Strip common prefixes like "kiliman/", "user/"
41
+ let name = branch.includes("/") ? branch.split("/").slice(1).join("/") : branch;
42
+ // Replace hyphens with spaces
43
+ name = name.replace(/-/g, " ");
44
+ // Capitalize first letter
45
+ return name.charAt(0).toUpperCase() + name.slice(1);
46
+ }
47
+
48
+ /**
49
+ * Push a branch to origin with upstream tracking and force-with-lease.
50
+ */
51
+ async function pushBranch(branch: string): Promise<boolean> {
52
+ try {
53
+ await $`git push -u --force-with-lease origin ${branch}`.quiet();
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ export default async function submit(args: string[]): Promise<void> {
61
+ if (args.includes("--help")) {
62
+ console.log(HELP);
63
+ return;
64
+ }
65
+
66
+ const draftFlag = args.includes("--draft") || args.includes("-d");
67
+ const dryRun = args.includes("--dry-run");
68
+ const noEdit = args.includes("--no-edit") || args.includes("-n") || isAutoYes();
69
+
70
+ const meta = await ensureMetadata();
71
+ const branch = await git.currentBranch();
72
+ const stackName = findStackForBranch(meta, branch);
73
+
74
+ if (!stackName) {
75
+ p.cancel(`Branch ${pc.blue(branch)} not found in any stack`);
76
+ process.exit(1);
77
+ }
78
+
79
+ const stack = meta.stacks[stackName]!;
80
+ const ordered = getOrderedBranches(stack);
81
+
82
+ // Determine downstack scope: branches from trunk up to and including current
83
+ const currentIndex = ordered.indexOf(branch);
84
+ const scope = currentIndex >= 0 ? ordered.slice(0, currentIndex + 1) : ordered;
85
+
86
+ p.intro(pc.cyan("Submit Stack"));
87
+ p.log.info(`Stack: ${pc.yellow(stackName)}`);
88
+ p.log.info(
89
+ `Scope: ${scope.length} of ${ordered.length} branch(es) (downstack from ${pc.blue(branch)})`,
90
+ );
91
+ console.log();
92
+
93
+ if (dryRun) {
94
+ p.log.info(pc.dim("Dry run — no changes will be made"));
95
+ console.log();
96
+ }
97
+
98
+ // ── Phase 1: Push branches and create/update PRs ──
99
+ let pushed = 0;
100
+ let created = 0;
101
+ let existing = 0;
102
+ let metadataChanged = false;
103
+
104
+ for (const branchName of scope) {
105
+ const branchMeta = stack.branches[branchName]!;
106
+ const parentBranch = branchMeta.parent;
107
+
108
+ // ── Push ──
109
+ if (dryRun) {
110
+ p.log.step(`${pc.dim("push")} ${branchName} → origin/${branchName}`);
111
+ } else {
112
+ const s = p.spinner();
113
+ s.start(`Pushing ${pc.blue(branchName)}...`);
114
+ const ok = await pushBranch(branchName);
115
+ if (ok) {
116
+ s.stop(`Pushed ${pc.blue(branchName)}`);
117
+ pushed++;
118
+ } else {
119
+ s.stop(pc.red(`Failed to push ${branchName}`));
120
+ p.cancel(`Push failed for ${pc.blue(branchName)}. Aborting.`);
121
+ process.exit(1);
122
+ }
123
+ }
124
+
125
+ // ── Check for existing PR ──
126
+ let prNumber = branchMeta.pr ?? null;
127
+
128
+ if (!prNumber && !dryRun) {
129
+ prNumber = await getPrNumber(branchName);
130
+ if (prNumber) {
131
+ // Found an existing PR not tracked in metadata — save it
132
+ branchMeta.pr = prNumber;
133
+ metadataChanged = true;
134
+ }
135
+ }
136
+
137
+ if (prNumber) {
138
+ // ── Existing PR: update base if needed ──
139
+ existing++;
140
+ if (dryRun) {
141
+ p.log.step(`${pc.dim("exists")} PR #${prNumber} for ${branchName}`);
142
+ } else {
143
+ // Ensure the PR base is correct
144
+ try {
145
+ await $`gh pr edit ${prNumber} --base ${parentBranch}`.quiet();
146
+ } catch {
147
+ // Best effort — base might already be correct
148
+ }
149
+ p.log.info(`${pc.dim("✓")} PR #${prNumber} for ${pc.blue(branchName)}`);
150
+ }
151
+ } else {
152
+ // ── Create new PR ──
153
+ const defaultTitle = branchToTitle(branchName);
154
+ let title = defaultTitle;
155
+
156
+ if (!noEdit && !dryRun) {
157
+ const input = await p.text({
158
+ message: `PR title for ${pc.blue(branchName)}`,
159
+ initialValue: defaultTitle,
160
+ placeholder: defaultTitle,
161
+ });
162
+ if (p.isCancel(input)) {
163
+ p.cancel("Cancelled");
164
+ process.exit(0);
165
+ }
166
+ title = (input as string).trim() || defaultTitle;
167
+ }
168
+
169
+ if (dryRun) {
170
+ p.log.step(
171
+ `${pc.dim("create")} PR for ${branchName} → ${parentBranch} "${title}"${draftFlag ? " (draft)" : ""}`,
172
+ );
173
+ created++;
174
+ } else {
175
+ const s = p.spinner();
176
+ s.start(`Creating PR for ${pc.blue(branchName)}...`);
177
+
178
+ const ghArgs = [
179
+ "pr",
180
+ "create",
181
+ "--base",
182
+ parentBranch,
183
+ "--head",
184
+ branchName,
185
+ "--title",
186
+ title,
187
+ "--body",
188
+ "",
189
+ ];
190
+ if (draftFlag) ghArgs.push("--draft");
191
+
192
+ const proc = Bun.spawn(["gh", ...ghArgs], {
193
+ stdout: "pipe",
194
+ stderr: "pipe",
195
+ });
196
+ const output = await new Response(proc.stdout).text();
197
+ const exitCode = await proc.exited;
198
+
199
+ if (exitCode === 0) {
200
+ // Extract PR number from URL output
201
+ const prUrl = output.trim();
202
+ const prNumMatch = prUrl.match(/\/pull\/(\d+)/);
203
+ const newPrNumber = prNumMatch ? parseInt(prNumMatch[1]!, 10) : null;
204
+
205
+ if (newPrNumber) {
206
+ branchMeta.pr = newPrNumber;
207
+ metadataChanged = true;
208
+ s.stop(`Created PR #${newPrNumber} for ${pc.blue(branchName)}`);
209
+ } else {
210
+ s.stop(`Created PR for ${pc.blue(branchName)} (could not parse PR number)`);
211
+ }
212
+ created++;
213
+ } else {
214
+ const stderr = await new Response(proc.stderr).text();
215
+ s.stop(pc.red(`Failed to create PR for ${branchName}`));
216
+ p.log.error(stderr.trim());
217
+ }
218
+ }
219
+ }
220
+ }
221
+
222
+ // Save metadata if we discovered or created PRs
223
+ if (metadataChanged && !dryRun) {
224
+ await writeMetadata(meta);
225
+ }
226
+
227
+ console.log();
228
+
229
+ if (dryRun) {
230
+ p.outro(
231
+ pc.dim(
232
+ `Dry run complete: would push ${scope.length}, create ${created}, update ${existing} PR(s)`,
233
+ ),
234
+ );
235
+ return;
236
+ }
237
+
238
+ // ── Phase 2: Update stack visualization in all PR descriptions ──
239
+ p.log.info(pc.cyan("Updating stack visualization..."));
240
+
241
+ // Collect PR info for ALL branches in the stack (not just scope)
242
+ interface BranchPrInfo {
243
+ branch: string;
244
+ prNumber: number | null;
245
+ prTitle: string;
246
+ prUrl: string | null;
247
+ reviewEmojiStr: string;
248
+ }
249
+
250
+ const branchInfos: BranchPrInfo[] = [];
251
+
252
+ for (const branchName of ordered) {
253
+ const branchMeta = stack.branches[branchName]!;
254
+ const prNum = branchMeta.pr ?? null;
255
+ let prTitle = branchMeta.description || branchName;
256
+ let prUrl: string | null = null;
257
+ let review = "PENDING";
258
+
259
+ if (prNum) {
260
+ const info = await getPrInfo(prNum);
261
+ if (info) {
262
+ prTitle = info.title || prTitle;
263
+ prUrl = info.url;
264
+ review = info.reviewDecision || "PENDING";
265
+ }
266
+ }
267
+
268
+ branchInfos.push({
269
+ branch: branchName,
270
+ prNumber: prNum,
271
+ prTitle,
272
+ prUrl,
273
+ reviewEmojiStr: reviewEmoji(review),
274
+ });
275
+ }
276
+
277
+ // Update each PR that has a number
278
+ let vizUpdated = 0;
279
+
280
+ for (let i = 0; i < branchInfos.length; i++) {
281
+ const info = branchInfos[i]!;
282
+ if (!info.prNumber) continue;
283
+
284
+ const stackViz = buildStackViz(branchInfos, i);
285
+
286
+ const currentBody = await getPrBody(info.prNumber);
287
+ if (currentBody === null) continue;
288
+
289
+ // Remove existing stack section
290
+ let updatedBody = currentBody;
291
+ const stackSectionIdx = updatedBody.indexOf("### 📚 Stacked on");
292
+ if (stackSectionIdx !== -1) {
293
+ updatedBody = updatedBody.slice(0, stackSectionIdx).trimEnd();
294
+ }
295
+
296
+ // Append new stack section
297
+ const newBody = updatedBody ? `${updatedBody}\n\n${stackViz}` : stackViz;
298
+
299
+ const ok = await updatePrBody(info.prNumber, newBody);
300
+ if (ok) vizUpdated++;
301
+ }
302
+
303
+ // ── Summary ──
304
+ console.log();
305
+ p.outro(
306
+ pc.green(
307
+ `Done! Pushed ${pushed} branch(es), created ${created} PR(s), updated ${vizUpdated} description(s)`,
308
+ ),
309
+ );
310
+ }
@@ -3,7 +3,7 @@ import * as p from "@clack/prompts";
3
3
  import pc from "picocolors";
4
4
  import * as git from "../lib/git.ts";
5
5
  import { findStackForBranch, getOrderedBranches, saveRestackState } from "../lib/metadata.ts";
6
- import { ensureMetadata, ensureCleanWorkingTree } from "../lib/safety.ts";
6
+ import { ensureMetadata, ensureCleanWorkingTree, ensureValidStack } from "../lib/safety.ts";
7
7
  import { takeSnapshot } from "../lib/snapshot.ts";
8
8
  import { confirmAction } from "../lib/ui.ts";
9
9
 
@@ -36,6 +36,7 @@ with the base branch included.
36
36
  }
37
37
 
38
38
  const stack = meta.stacks[stackName]!;
39
+ await ensureValidStack(meta, stackName);
39
40
  const ordered = getOrderedBranches(stack);
40
41
 
41
42
  if (ordered.length === 0) {
@@ -98,7 +99,7 @@ with the base branch included.
98
99
  const parentRef = parent === "main" ? "origin/main" : parent;
99
100
  const mb = await git.mergeBase(branch, parentRef);
100
101
  if (mb) {
101
- const tagName = `stack-sync-base-${git.sanitizeBranchForTag(branch)}`;
102
+ const tagName = git.tempBaseTagName(branch);
102
103
  await git.createTag(tagName, mb);
103
104
  console.log(
104
105
  ` ${pc.green("✓")} Tagged base for ${branch}: ${pc.cyan(tagName)} (${mb.slice(0, 8)})`,
@@ -0,0 +1,79 @@
1
+ // gh-stack top — Jump to the tip of the stack
2
+ import * as p from "@clack/prompts";
3
+ import pc from "picocolors";
4
+ import * as git from "../lib/git.ts";
5
+ import {
6
+ findStackForBranch,
7
+ getOrderedBranches,
8
+ getChildren,
9
+ writeMetadata,
10
+ } from "../lib/metadata.ts";
11
+ import { ensureMetadata } from "../lib/safety.ts";
12
+
13
+ export default async function top(args: string[]): Promise<void> {
14
+ if (args.includes("--help")) {
15
+ console.log(`
16
+ gh-stack top — Jump to the tip of the stack
17
+
18
+ USAGE
19
+ gh-stack top
20
+
21
+ Switches to the topmost (leaf) branch of the current stack.
22
+ Prompts if there are multiple leaf branches.
23
+ `);
24
+ return;
25
+ }
26
+
27
+ const meta = await ensureMetadata();
28
+ const currentBranch = await git.currentBranch();
29
+ const stackName = findStackForBranch(meta, currentBranch);
30
+
31
+ if (!stackName) {
32
+ p.cancel(`Branch ${pc.yellow(currentBranch)} is not in any stack`);
33
+ process.exit(1);
34
+ }
35
+
36
+ const stack = meta.stacks[stackName]!;
37
+ const ordered = getOrderedBranches(stack);
38
+
39
+ // Find leaf branches (no children)
40
+ const leaves = ordered.filter((b) => getChildren(stack, b).length === 0);
41
+
42
+ if (leaves.length === 0) {
43
+ p.log.warn("No branches in stack");
44
+ return;
45
+ }
46
+
47
+ let target: string;
48
+ if (leaves.length === 1) {
49
+ target = leaves[0]!;
50
+ } else {
51
+ const result = await p.select({
52
+ message: "Multiple tips — which branch?",
53
+ options: leaves.map((l) => ({
54
+ value: l,
55
+ label: l,
56
+ hint: stack.branches[l]?.pr ? `#${stack.branches[l]!.pr}` : undefined,
57
+ })),
58
+ });
59
+ if (p.isCancel(result)) {
60
+ p.cancel("Cancelled");
61
+ process.exit(0);
62
+ }
63
+ target = result as string;
64
+ }
65
+
66
+ if (target === currentBranch) {
67
+ p.log.info(`Already at the top: ${pc.yellow(currentBranch)}`);
68
+ return;
69
+ }
70
+
71
+ await git.checkout(target);
72
+
73
+ // Update tracking
74
+ stack.last_branch = target;
75
+ meta.current_stack = stackName;
76
+ await writeMetadata(meta);
77
+
78
+ p.log.success(`On ${pc.yellow(target)}`);
79
+ }
@@ -1,5 +1,6 @@
1
1
  // gh-stack undo — Restore from last snapshot
2
2
  import * as p from "@clack/prompts";
3
+ import * as git from "../lib/git.ts";
3
4
  import pc from "picocolors";
4
5
  import { ensureMetadata, ensureCleanWorkingTree } from "../lib/safety.ts";
5
6
  import { getLastSnapshot, popSnapshot } from "../lib/snapshot.ts";
@@ -47,10 +48,21 @@ a destructive operation (restack, merge, sync, remove).
47
48
  process.exit(0);
48
49
  }
49
50
 
51
+ let activeBranch: string | null = null;
52
+ try {
53
+ activeBranch = await git.currentBranch();
54
+ } catch {
55
+ // Detached HEAD is fine — we can restore all branches with branch -f.
56
+ }
57
+
50
58
  // Restore each branch HEAD
51
59
  for (const [branch, sha] of Object.entries(snapshot.branches)) {
52
60
  try {
53
- await $`git branch -f ${branch} ${sha}`.quiet();
61
+ if (branch === activeBranch) {
62
+ await $`git reset --hard ${sha}`.quiet();
63
+ } else {
64
+ await $`git branch -f ${branch} ${sha}`.quiet();
65
+ }
54
66
  p.log.success(`Restored ${pc.yellow(branch)} → ${sha.slice(0, 8)}`);
55
67
  } catch {
56
68
  p.log.error(`Failed to restore ${branch}`);
@@ -0,0 +1,82 @@
1
+ // gh-stack up — Move to child branch (upstack)
2
+ import * as p from "@clack/prompts";
3
+ import pc from "picocolors";
4
+ import * as git from "../lib/git.ts";
5
+ import { findStackForBranch, getChildren, writeMetadata } from "../lib/metadata.ts";
6
+ import { ensureMetadata } from "../lib/safety.ts";
7
+
8
+ export default async function up(args: string[]): Promise<void> {
9
+ if (args.includes("--help")) {
10
+ console.log(`
11
+ gh-stack up — Move to child branch (upstack)
12
+
13
+ USAGE
14
+ gh-stack up [steps]
15
+
16
+ OPTIONS
17
+ [steps] Number of levels to move up (default: 1)
18
+ `);
19
+ return;
20
+ }
21
+
22
+ const steps = parseInt(args[0] || "1", 10);
23
+ if (isNaN(steps) || steps < 1) {
24
+ p.cancel("Invalid step count");
25
+ process.exit(1);
26
+ }
27
+
28
+ const meta = await ensureMetadata();
29
+ let currentBranch = await git.currentBranch();
30
+ const stackName = findStackForBranch(meta, currentBranch);
31
+
32
+ if (!stackName) {
33
+ p.cancel(`Branch ${pc.yellow(currentBranch)} is not in any stack`);
34
+ process.exit(1);
35
+ }
36
+
37
+ const stack = meta.stacks[stackName]!;
38
+
39
+ for (let i = 0; i < steps; i++) {
40
+ const children = getChildren(stack, currentBranch);
41
+
42
+ if (children.length === 0) {
43
+ if (i === 0) {
44
+ p.log.warn(`${pc.yellow(currentBranch)} is already at the top of the stack`);
45
+ } else {
46
+ p.log.info(`Reached top of stack at ${pc.yellow(currentBranch)} after ${i} step(s)`);
47
+ }
48
+ return;
49
+ }
50
+
51
+ let target: string;
52
+ if (children.length === 1) {
53
+ target = children[0]!;
54
+ } else {
55
+ // Multiple children — prompt
56
+ const result = await p.select({
57
+ message: "Multiple children — which branch?",
58
+ options: children.map((c) => ({
59
+ value: c,
60
+ label: c,
61
+ hint: stack.branches[c]?.pr ? `#${stack.branches[c]!.pr}` : undefined,
62
+ })),
63
+ });
64
+ if (p.isCancel(result)) {
65
+ p.cancel("Cancelled");
66
+ process.exit(0);
67
+ }
68
+ target = result as string;
69
+ }
70
+
71
+ currentBranch = target;
72
+ }
73
+
74
+ await git.checkout(currentBranch);
75
+
76
+ // Update tracking
77
+ stack.last_branch = currentBranch;
78
+ meta.current_stack = stackName;
79
+ await writeMetadata(meta);
80
+
81
+ p.log.success(`On ${pc.yellow(currentBranch)}`);
82
+ }
package/src/index.ts CHANGED
@@ -7,26 +7,28 @@ if (process.env.GH_STACK_NO_COLOR) {
7
7
  }
8
8
 
9
9
  import { ensureGitRepo } from "./lib/safety.ts";
10
+ import { parseCliArgs } from "./lib/cli.ts";
10
11
  import { setAutoYes } from "./lib/ui.ts";
11
12
 
12
13
  const { version: VERSION } = await import("../package.json");
13
14
 
14
15
  const args = process.argv.slice(2);
15
- const command = args[0] || "show";
16
+ const parsed = parseCliArgs(args, process.env);
17
+ const command = parsed.command;
16
18
 
17
19
  // Handle global flags
18
- if (args.includes("--version") || args.includes("-V")) {
20
+ if (parsed.showVersion) {
19
21
  console.log(`gh-stack v${VERSION}`);
20
22
  process.exit(0);
21
23
  }
22
24
 
23
- if (args.includes("--help") && !args[0]) {
25
+ if (parsed.showGlobalHelp) {
24
26
  printHelp();
25
27
  process.exit(0);
26
28
  }
27
29
 
28
30
  // Global --yes flag: skip all interactive confirmations (for agents/CI)
29
- if (args.includes("--yes") || args.includes("-y") || process.env.GH_STACK_YES === "1") {
31
+ if (parsed.autoYes) {
30
32
  setAutoYes(true);
31
33
  }
32
34
 
@@ -34,30 +36,30 @@ if (args.includes("--yes") || args.includes("-y") || process.env.GH_STACK_YES ==
34
36
  await ensureGitRepo();
35
37
 
36
38
  // Route to subcommand — strip global flags from command args
37
- const commandArgs = args.slice(1).filter((a) => a !== "--yes" && a !== "-y");
39
+ const commandArgs = parsed.commandArgs;
38
40
 
39
41
  switch (command) {
40
- case "show":
41
- await (await import("./commands/show.ts")).default(commandArgs);
42
+ case "log":
43
+ await (await import("./commands/log.ts")).default(commandArgs);
42
44
  break;
43
45
 
44
46
  case "init":
45
47
  await (await import("./commands/init.ts")).default(commandArgs);
46
48
  break;
47
49
 
48
- case "add":
49
- await (await import("./commands/add.ts")).default(commandArgs);
50
+ case "create":
51
+ await (await import("./commands/create.ts")).default(commandArgs);
50
52
  break;
51
53
 
52
- case "remove":
53
- await (await import("./commands/remove.ts")).default(commandArgs);
54
+ case "delete":
55
+ await (await import("./commands/delete.ts")).default(commandArgs);
54
56
  break;
55
57
 
56
- case "switch":
57
- await (await import("./commands/switch.ts")).default(commandArgs);
58
+ case "checkout":
59
+ case "co":
60
+ await (await import("./commands/checkout.ts")).default(commandArgs);
58
61
  break;
59
62
 
60
- case "list":
61
63
  case "ls":
62
64
  await (await import("./commands/list.ts")).default(commandArgs);
63
65
  break;
@@ -71,8 +73,8 @@ switch (command) {
71
73
  await (await import("./commands/sync.ts")).default(commandArgs);
72
74
  break;
73
75
 
74
- case "update-prs":
75
- await (await import("./commands/update-prs.ts")).default(commandArgs);
76
+ case "submit":
77
+ await (await import("./commands/submit.ts")).default(commandArgs);
76
78
  break;
77
79
 
78
80
  case "status":
@@ -91,6 +93,22 @@ switch (command) {
91
93
  await (await import("./commands/archive.ts")).default(commandArgs);
92
94
  break;
93
95
 
96
+ case "up":
97
+ await (await import("./commands/up.ts")).default(commandArgs);
98
+ break;
99
+
100
+ case "down":
101
+ await (await import("./commands/down.ts")).default(commandArgs);
102
+ break;
103
+
104
+ case "top":
105
+ await (await import("./commands/top.ts")).default(commandArgs);
106
+ break;
107
+
108
+ case "bottom":
109
+ await (await import("./commands/bottom.ts")).default(commandArgs);
110
+ break;
111
+
94
112
  case "--help":
95
113
  case "help":
96
114
  printHelp();
@@ -109,18 +127,34 @@ gh-stack v${VERSION} — Stacked PR manager for squash-merge workflows
109
127
  ${bold("USAGE")}
110
128
  gh-stack <command> [options]
111
129
 
112
- ${bold("COMMANDS")}
113
- ${green("show")} Display current stack tree ${dim("(default)")}
114
- ${green("list")} List branches with numbers ${dim("(alias: ls)")}
115
- ${green("init")} Create a new stack
116
- ${green("add")} Add a branch to the current stack
117
- ${green("remove")} Remove a branch from the stack
118
- ${green("switch")} Switch branch or stack
130
+ ${bold("TERMS")}
131
+ trunk Base branch of the repository (usually main)
132
+ stack A chain of dependent branches
133
+ upstack Branches that depend on the current branch (children)
134
+ downstack Branches the current branch depends on (ancestors)
135
+
136
+ ${bold("CORE WORKFLOW")}
137
+ ${green("init")} Create a new stack from the current branch
138
+ ${green("create")} Create a new branch and add to the stack
139
+ ${green("submit")} Push branches and create/update PRs
140
+ ${green("log")} Display current stack tree ${dim("(default)")}
141
+
142
+ ${bold("STACK NAVIGATION")}
143
+ ${green("checkout")} Switch to a branch or stack ${dim("(alias: co)")}
144
+ ${green("up")} Move to child branch (upstack)
145
+ ${green("down")} Move to parent branch (downstack)
146
+ ${green("top")} Jump to the tip of the stack
147
+ ${green("bottom")} Jump to the base of the stack
148
+ ${green("ls")} List branches with numbers
149
+
150
+ ${bold("STACK MANAGEMENT")}
119
151
  ${green("restack")} Rebase children onto updated parents
120
152
  ${green("sync")} Sync base with main + restack all
121
- ${green("update-prs")} Update PR descriptions with stack viz
122
- ${green("status")} PR dashboard (CI, reviews)
123
153
  ${green("merge")} Local squash-merge top-down
154
+ ${green("delete")} Remove a branch from the stack
155
+
156
+ ${bold("INFO & MAINTENANCE")}
157
+ ${green("status")} PR dashboard (CI, reviews)
124
158
  ${green("undo")} Restore from last snapshot
125
159
  ${green("archive")} Manage archived stacks
126
160