gh-stack 0.15.0 → 0.17.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 +64 -20
- package/package.json +1 -1
- package/src/commands/add.ts +107 -0
- package/src/commands/bottom.ts +2 -2
- package/src/commands/checkout.ts +9 -8
- package/src/commands/down.ts +2 -2
- package/src/commands/learn.ts +152 -0
- package/src/commands/merge.ts +174 -98
- package/src/commands/restack.ts +57 -13
- package/src/commands/sync.ts +3 -1
- package/src/commands/top.ts +2 -2
- package/src/commands/up.ts +2 -2
- package/src/index.ts +16 -2
- package/src/lib/git.ts +45 -1
- package/src/lib/md.d.ts +7 -0
- package/src/lib/safety.ts +31 -0
- package/src/lib/skill.ts +22 -0
- package/src/lib/using-gh-stack.md +260 -0
package/src/index.ts
CHANGED
|
@@ -46,8 +46,12 @@ if (parsed.plain) {
|
|
|
46
46
|
setPlain(true);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// Ensure we're in a git repo for all commands
|
|
50
|
-
|
|
49
|
+
// Ensure we're in a git repo for all commands — except `learn`, which just
|
|
50
|
+
// emits/installs the agent skill and is useful outside any repo (e.g. an agent
|
|
51
|
+
// onboarding before cloning, or `learn --skill --global`).
|
|
52
|
+
if (command !== "learn") {
|
|
53
|
+
await ensureGitRepo();
|
|
54
|
+
}
|
|
51
55
|
|
|
52
56
|
// Route to subcommand — strip global flags from command args
|
|
53
57
|
const commandArgs = parsed.commandArgs;
|
|
@@ -65,6 +69,10 @@ switch (command) {
|
|
|
65
69
|
await (await import("./commands/create.ts")).default(commandArgs);
|
|
66
70
|
break;
|
|
67
71
|
|
|
72
|
+
case "add":
|
|
73
|
+
await (await import("./commands/add.ts")).default(commandArgs);
|
|
74
|
+
break;
|
|
75
|
+
|
|
68
76
|
case "delete":
|
|
69
77
|
await (await import("./commands/delete.ts")).default(commandArgs);
|
|
70
78
|
break;
|
|
@@ -143,6 +151,10 @@ switch (command) {
|
|
|
143
151
|
await (await import("./commands/bottom.ts")).default(commandArgs);
|
|
144
152
|
break;
|
|
145
153
|
|
|
154
|
+
case "learn":
|
|
155
|
+
await (await import("./commands/learn.ts")).default(commandArgs);
|
|
156
|
+
break;
|
|
157
|
+
|
|
146
158
|
case "--help":
|
|
147
159
|
case "help":
|
|
148
160
|
printHelp();
|
|
@@ -170,6 +182,7 @@ ${bold("TERMS")}
|
|
|
170
182
|
${bold("CORE WORKFLOW")}
|
|
171
183
|
${green("init")} Create a new stack from the current branch
|
|
172
184
|
${green("create")} Create a new branch and add to the stack
|
|
185
|
+
${green("add")} Track the current local branch in a stack ${dim("(no push/PR)")}
|
|
173
186
|
${green("submit")} Push branches and create/update PRs
|
|
174
187
|
${green("log")} Display current stack tree ${dim("(default)")}
|
|
175
188
|
|
|
@@ -196,6 +209,7 @@ ${bold("INFO & MAINTENANCE")}
|
|
|
196
209
|
${green("archive")} Manage archived stacks
|
|
197
210
|
${green("update-prs")} Refresh stack visualization in PR descriptions
|
|
198
211
|
${green("doctor")} Migrate & repair stack metadata
|
|
212
|
+
${green("learn")} Print the agent skill (--skill installs it)
|
|
199
213
|
|
|
200
214
|
${bold("GLOBAL OPTIONS")}
|
|
201
215
|
--yes, -y Skip confirmations ${dim("(for agents/CI)")}
|
package/src/lib/git.ts
CHANGED
|
@@ -38,6 +38,14 @@ export async function gitDir(): Promise<string> {
|
|
|
38
38
|
return result.trim();
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Get the absolute path to the repository's working-tree root.
|
|
43
|
+
*/
|
|
44
|
+
export async function repoRoot(): Promise<string> {
|
|
45
|
+
const result = await $`git rev-parse --show-toplevel`.text();
|
|
46
|
+
return result.trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
/**
|
|
42
50
|
* Check if working tree is clean (no uncommitted changes).
|
|
43
51
|
* Refreshes the index first to avoid false positives from stale stat info.
|
|
@@ -78,12 +86,25 @@ export async function isRebaseInProgress(): Promise<boolean> {
|
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
/**
|
|
81
|
-
* Checkout a branch.
|
|
89
|
+
* Checkout a branch. Throws a raw ShellError on failure — only use this where a
|
|
90
|
+
* failure is unexpected (e.g. after ensureCleanWorkingTree). For user-facing
|
|
91
|
+
* branch switching, prefer tryCheckout so a dirty-tree refusal can be rendered
|
|
92
|
+
* as a clean message instead of a stack trace.
|
|
82
93
|
*/
|
|
83
94
|
export async function checkout(branch: string): Promise<void> {
|
|
84
95
|
await $`git checkout ${branch}`.quiet();
|
|
85
96
|
}
|
|
86
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Attempt to checkout a branch without throwing. Returns whether it succeeded
|
|
100
|
+
* and git's stderr (which carries the "local changes would be overwritten"
|
|
101
|
+
* message and the offending file list when a dirty tree blocks the switch).
|
|
102
|
+
*/
|
|
103
|
+
export async function tryCheckout(branch: string): Promise<{ ok: boolean; stderr: string }> {
|
|
104
|
+
const { exitCode, stderr } = await $`git checkout ${branch}`.quiet().nothrow();
|
|
105
|
+
return { ok: exitCode === 0, stderr: stderr.toString() };
|
|
106
|
+
}
|
|
107
|
+
|
|
87
108
|
/**
|
|
88
109
|
* Create and checkout a new branch from the current HEAD.
|
|
89
110
|
*/
|
|
@@ -149,6 +170,29 @@ export async function pushStackedBranch(branch: string): Promise<boolean> {
|
|
|
149
170
|
return exitCode === 0;
|
|
150
171
|
}
|
|
151
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Read the actual tip sha of every branch on origin in a single `ls-remote`
|
|
175
|
+
* call. This queries the remote directly, so it is **authoritative** — unlike
|
|
176
|
+
* `rev-parse origin/<branch>`, which reads the local remote-tracking ref that a
|
|
177
|
+
* force-push can leave stale (a fresh fetch would correct it). Use this when a
|
|
178
|
+
* correct answer about the remote's real state matters (e.g. the post-restack
|
|
179
|
+
* sync verification — #23). Returns a map of branch name → sha.
|
|
180
|
+
*/
|
|
181
|
+
export async function remoteHeads(): Promise<Map<string, string>> {
|
|
182
|
+
const heads = new Map<string, string>();
|
|
183
|
+
const { exitCode, stdout } = await $`git ls-remote --heads origin`.nothrow().quiet();
|
|
184
|
+
if (exitCode !== 0) return heads;
|
|
185
|
+
const prefix = "refs/heads/";
|
|
186
|
+
for (const line of stdout.toString().split("\n")) {
|
|
187
|
+
const tab = line.indexOf("\t");
|
|
188
|
+
if (tab === -1) continue;
|
|
189
|
+
const sha = line.slice(0, tab).trim();
|
|
190
|
+
const ref = line.slice(tab + 1).trim();
|
|
191
|
+
if (sha && ref.startsWith(prefix)) heads.set(ref.slice(prefix.length), sha);
|
|
192
|
+
}
|
|
193
|
+
return heads;
|
|
194
|
+
}
|
|
195
|
+
|
|
152
196
|
/**
|
|
153
197
|
* Check if a branch exists on the remote.
|
|
154
198
|
*/
|
package/src/lib/md.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Bun bundles `*.md` files imported with `{ type: "text" }` into the binary as
|
|
2
|
+
// their raw string contents (survives `bun build --compile`). This declaration
|
|
3
|
+
// lets TypeScript type those imports.
|
|
4
|
+
declare module "*.md" {
|
|
5
|
+
const content: string;
|
|
6
|
+
export default content;
|
|
7
|
+
}
|
package/src/lib/safety.ts
CHANGED
|
@@ -115,6 +115,37 @@ export async function ensureCleanWorkingTree(): Promise<void> {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Switch branches for a user-facing navigation command, rendering a clean
|
|
120
|
+
* message (and exiting 1) when git refuses — most commonly because local
|
|
121
|
+
* changes would be overwritten. Avoids leaking a raw ShellError stack trace.
|
|
122
|
+
*
|
|
123
|
+
* Note: this deliberately does NOT pre-check for a clean tree. git happily
|
|
124
|
+
* carries uncommitted changes across a switch when they don't conflict with the
|
|
125
|
+
* target branch, and stack navigation often wants to bring WIP along — so we
|
|
126
|
+
* let git decide and only intervene on an actual refusal.
|
|
127
|
+
*/
|
|
128
|
+
export async function checkoutOrExit(branch: string): Promise<void> {
|
|
129
|
+
const { ok, stderr } = await git.tryCheckout(branch);
|
|
130
|
+
if (ok) return;
|
|
131
|
+
|
|
132
|
+
if (stderr.includes("would be overwritten by checkout")) {
|
|
133
|
+
const files = stderr
|
|
134
|
+
.split("\n")
|
|
135
|
+
.filter((line) => line.startsWith("\t"))
|
|
136
|
+
.map((line) => ` ${pc.yellow(line.trim())}`)
|
|
137
|
+
.join("\n");
|
|
138
|
+
p.cancel(
|
|
139
|
+
`Can't switch to ${pc.yellow(branch)} — local changes would be overwritten:\n\n${files}\n\n Commit or stash them first:\n ${pc.green("git stash")} ${pc.dim("(or git commit)")}`,
|
|
140
|
+
);
|
|
141
|
+
} else {
|
|
142
|
+
p.cancel(
|
|
143
|
+
`Can't switch to ${pc.yellow(branch)}:\n\n ${stderr.trim() || "git checkout failed"}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
|
|
118
149
|
/**
|
|
119
150
|
* Ensure we're not on main.
|
|
120
151
|
*/
|
package/src/lib/skill.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Canonical, version-stamped agent skill for gh-stack.
|
|
2
|
+
//
|
|
3
|
+
// The skill text is a plain Markdown file — `using-gh-stack.md` — bundled into
|
|
4
|
+
// the binary at build time via Bun's `type: "text"` import (it survives
|
|
5
|
+
// `bun build --compile`). Editing the skill means editing that .md file; this
|
|
6
|
+
// module only stamps the running version into the `{{VERSION}}` placeholder.
|
|
7
|
+
//
|
|
8
|
+
// It is the SINGLE SOURCE OF TRUTH for "how an agent should drive gh-stack":
|
|
9
|
+
// `gh-stack learn` prints it, `gh-stack learn --skill` installs it. Update the
|
|
10
|
+
// .md whenever a command or flag changes and the next release ships it.
|
|
11
|
+
import skillTemplate from "./using-gh-stack.md" with { type: "text" };
|
|
12
|
+
|
|
13
|
+
export const SKILL_NAME = "using-gh-stack";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build the full skill document, stamped with the running gh-stack version so
|
|
17
|
+
* an installed copy always records which release generated it (and can be
|
|
18
|
+
* refreshed with `gh-stack learn --skill`).
|
|
19
|
+
*/
|
|
20
|
+
export function skillContent(version: string): string {
|
|
21
|
+
return skillTemplate.replaceAll("{{VERSION}}", version);
|
|
22
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: using-gh-stack
|
|
3
|
+
description: "MUST be used when working with stacked PRs. gh-stack is a CLI tool for managing stacked PR workflows in squash-merge repos (a Graphite alternative). Covers creating stacks, pushing/creating PRs with submit, navigating (up/down/top/bottom), syncing with main, restacking, merging, and agent/CI usage. Triggers on: stacked PR, gh-stack, create stack, submit stack, push stack, restack, sync stack, stack navigation, up down top bottom, PR dependencies, stack visualization, manage PRs, stacked branches."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Using gh-stack
|
|
7
|
+
|
|
8
|
+
> Generated by `gh-stack learn` from **gh-stack v{{VERSION}}**.
|
|
9
|
+
> This file is produced by the binary — don't hand-edit it. Refresh after
|
|
10
|
+
> upgrading with `gh-stack learn --skill`.
|
|
11
|
+
|
|
12
|
+
`gh-stack` is a CLI for managing **stacked PRs** in squash-merge repositories
|
|
13
|
+
(a Graphite/`gt` alternative). A _stack_ is a chain of dependent branches, each
|
|
14
|
+
its own PR, sitting on top of the trunk (usually `main`).
|
|
15
|
+
|
|
16
|
+
**Install:** `npm install -g gh-stack` (or `bun install -g gh-stack`)
|
|
17
|
+
|
|
18
|
+
## Terms
|
|
19
|
+
|
|
20
|
+
| Term | Meaning |
|
|
21
|
+
| ------------- | ------------------------------------ |
|
|
22
|
+
| **trunk** | Base branch (usually `main`) |
|
|
23
|
+
| **stack** | A chain of dependent branches |
|
|
24
|
+
| **upstack** | Child branches (depend on current) |
|
|
25
|
+
| **downstack** | Parent branches (current depends on) |
|
|
26
|
+
|
|
27
|
+
## The current stack follows your current branch
|
|
28
|
+
|
|
29
|
+
There is no "switch stack" state to manage — **gh-stack always operates on the
|
|
30
|
+
stack that contains the branch you're on.** It resolves this from your current
|
|
31
|
+
git branch: the branch's own stack, or (for a new branch built on top of one)
|
|
32
|
+
the nearest tracked ancestor's stack. Every command (`log`, `submit`, `status`,
|
|
33
|
+
`restack`, …) acts on that stack automatically.
|
|
34
|
+
|
|
35
|
+
So to switch stacks, just switch branches — there's nothing gh-stack-specific to
|
|
36
|
+
run:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git switch other-feature-branch # any branch on the other stack
|
|
40
|
+
gh-stack log # now shows that stack
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or use the interactive picker when you don't remember the branch names:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
gh-stack checkout --stack # pick a stack, jump to it (alias: co --stack)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
(Plain `gh-stack checkout` picks a _branch within the current stack_; add
|
|
50
|
+
`--stack` to pick a _different stack_.)
|
|
51
|
+
|
|
52
|
+
## Agent/CI mode — read this first
|
|
53
|
+
|
|
54
|
+
Run non-interactively with `--yes`/`-y` or `GH_STACK_YES=1` (also enables
|
|
55
|
+
`--plain`: no spinners/colors/box-drawing — clean for log capture).
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
export GH_STACK_YES=1 # skip every confirmation, plain output
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- **Discover topology without parsing internals:** `gh-stack stacks --json`
|
|
62
|
+
(network-free) or `gh-stack status --json` (adds PR/CI/review state).
|
|
63
|
+
- **Always pass `-t`/`-b` to `submit`** so PRs get real titles/bodies instead
|
|
64
|
+
of generic auto-generated ones (see below).
|
|
65
|
+
- Commands that rebase (`restack`, `sync`, `merge`) **refuse to run on a dirty
|
|
66
|
+
tree** — commit or stash first.
|
|
67
|
+
|
|
68
|
+
## Core workflow
|
|
69
|
+
|
|
70
|
+
### 1. Start a stack
|
|
71
|
+
|
|
72
|
+
**From existing branches** (e.g. `main → feat-1 → feat-2`) — go to the TOP;
|
|
73
|
+
`init` walks down and reconstructs the chain automatically:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
git checkout feat-2
|
|
77
|
+
gh-stack init # detects main → feat-1 → feat-2, adopts existing PRs
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**From scratch:**
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git checkout -b kiliman/first-WEB-1234
|
|
84
|
+
gh-stack init # create stack, add current branch
|
|
85
|
+
gh-stack create kiliman/second-WEB-5678 # new branch stacked on top
|
|
86
|
+
# ...work, commit...
|
|
87
|
+
gh-stack create kiliman/third-WEB-9012 # stack another
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`init` options: `--name <stack>`, `--parent <trunk>`, `--description <text>`.
|
|
91
|
+
|
|
92
|
+
**Track a local WIP branch without publishing it — `gh-stack add`.** If you
|
|
93
|
+
created a branch with `git checkout -b` and want to `sync`/`restack` it _before_
|
|
94
|
+
opening a PR, `add` registers it in a stack (network-free — no push, no PR):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
gh-stack add # track the current branch
|
|
98
|
+
gh-stack add <branch> # track a named branch
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
It walks local ancestry: a branch sitting on top of an existing stack is adopted
|
|
102
|
+
into it (parent auto-detected); otherwise a new stack is created from the chain.
|
|
103
|
+
This resolves the catch-22 where `sync`/`restack` said "not in any stack" for a
|
|
104
|
+
tip you hadn't submitted yet. (`submit` still auto-tracks too — `add` is just the
|
|
105
|
+
no-publish path.)
|
|
106
|
+
|
|
107
|
+
### 2. Push & create PRs — `submit`
|
|
108
|
+
|
|
109
|
+
`submit` is the workhorse: **idempotent, scope-aware, self-healing**. It pushes
|
|
110
|
+
changed branches with `--force-with-lease` (skips up-to-date ones), creates
|
|
111
|
+
missing PRs with the correct base, fixes PR bases, and refreshes the stack
|
|
112
|
+
visualization in every PR body.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
gh-stack submit # push downstack + create/update PRs
|
|
116
|
+
gh-stack submit -d # create new PRs as drafts
|
|
117
|
+
gh-stack submit --dry-run # preview, no side effects
|
|
118
|
+
gh-stack submit --restack # then propagate up to children (see below)
|
|
119
|
+
gh-stack submit -t "<title>" -b "<body>" # explicit title/body for new PRs
|
|
120
|
+
gh-stack submit -t "<title>" --body-file /tmp/body.md
|
|
121
|
+
gh-stack submit -n # auto-generate titles (generic — avoid)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Agents: provide title + body explicitly.** `--title` implies `--no-edit`
|
|
125
|
+
(no prompts); title/body apply to all new PRs created in scope:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
gh-stack submit \
|
|
129
|
+
-t "Store raw transcription JSON and WebVTT [WEB-7409]" \
|
|
130
|
+
-b "$(cat <<'EOF'
|
|
131
|
+
## Summary
|
|
132
|
+
- Store raw transcription JSON alongside pre-rendered WebVTT
|
|
133
|
+
|
|
134
|
+
## Test plan
|
|
135
|
+
- [ ] Verify both formats persist
|
|
136
|
+
EOF
|
|
137
|
+
)"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**`submit -t`/`-b` also updates an existing PR in place** — pass them while on a
|
|
141
|
+
branch that already has a PR to change just that one (visualization-safe).
|
|
142
|
+
|
|
143
|
+
**`--restack`** chains a restack after submit. `submit` owns DOWNSTACK (trunk →
|
|
144
|
+
current); `restack` owns UPSTACK (the children). Together they're the everyday
|
|
145
|
+
downstack-fix loop in one command: _drop downstack → fix → commit →
|
|
146
|
+
`gh-stack submit --restack`_ pushes the fix AND rebases the children onto it.
|
|
147
|
+
|
|
148
|
+
### 3. Navigate
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
gh-stack log # stack tree (default command)
|
|
152
|
+
gh-stack ls # numbered branch list
|
|
153
|
+
gh-stack up [n] # toward children (upstack)
|
|
154
|
+
gh-stack down [n] # toward parents (downstack)
|
|
155
|
+
gh-stack top # tip (leaf)
|
|
156
|
+
gh-stack bottom # base (first branch above trunk)
|
|
157
|
+
gh-stack checkout # interactive picker (alias: co)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 4. Sync & restack
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
gh-stack sync # fetch trunk, rebase base onto it, restack ALL children
|
|
164
|
+
gh-stack restack # rebase current branch + descendants onto their parents
|
|
165
|
+
gh-stack restack --dry-run
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**⚠️ Restack propagates UPWARD from the current branch.** Run it from the
|
|
169
|
+
**lowest branch you changed**, never from the top:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
gh-stack bottom # go to the changed branch
|
|
173
|
+
# ...edit, commit...
|
|
174
|
+
gh-stack submit # push it
|
|
175
|
+
gh-stack restack # propagates the change UP through children
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Changing multiple branches?** Finish each one fully before moving up —
|
|
179
|
+
`edit → test → commit → push → restack`, then `gh-stack up` and repeat. Don't
|
|
180
|
+
edit several branches then restack once at the end (only the last branch ends
|
|
181
|
+
up pushed).
|
|
182
|
+
|
|
183
|
+
**On rebase conflict:** resolve, `git add <files>`, `git rebase --continue`,
|
|
184
|
+
then `gh-stack restack --resume` to finish the remaining branches.
|
|
185
|
+
|
|
186
|
+
**`sync` is self-correcting:** if you merged the bottom PR in the GitHub UI,
|
|
187
|
+
`sync` detects it and advances the stack past the merged branch (re-rooting onto
|
|
188
|
+
trunk) instead of replaying squashed commits and conflicting.
|
|
189
|
+
|
|
190
|
+
### 5. Status
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
gh-stack status # PR dashboard: CI, reviews, merge readiness
|
|
194
|
+
gh-stack status --current # current stack only
|
|
195
|
+
gh-stack status --json # structured, for agents
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### 6. Merge & ship
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
gh-stack merge # collapse the WHOLE stack into one squash commit → trunk
|
|
202
|
+
gh-stack merge --approved # collapse only the approved run (base..highest-approved)
|
|
203
|
+
# into one commit; leave the unapproved tail in place
|
|
204
|
+
gh-stack merge --base # land ONLY the bottom PR → trunk, re-root the next branch
|
|
205
|
+
gh-stack merge --dry-run
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Merge is **top-down everywhere** (collapse PRn → PRn-1 → … → base PR, then base → trunk):
|
|
209
|
+
|
|
210
|
+
- **`merge`**: the whole stack is approved → lands as a single squash commit.
|
|
211
|
+
- **`merge --approved`**: collapse the **contiguous-from-base approved run** into
|
|
212
|
+
one commit on trunk. Approval is read from the base up — the first un-approved
|
|
213
|
+
PR caps the run (unapproved PR4 stops at PR3 even if PR5 is approved). The
|
|
214
|
+
unapproved tail is left in place; after the base lands, run `gh-stack sync` to
|
|
215
|
+
re-root the tail onto trunk (it does not block on CI).
|
|
216
|
+
- **`merge --base`**: squash-merge **only the bottom PR** into trunk and re-root
|
|
217
|
+
the next branch onto trunk — one PR per run, re-run for the next. Use this to
|
|
218
|
+
land reviewed PRs one at a time. Auto-advances past a bottom PR already merged
|
|
219
|
+
outside gh-stack.
|
|
220
|
+
- **`--collapse`** (with `merge` or `merge --approved`): stop after collapsing
|
|
221
|
+
into the base PR — review on GitHub, then re-run `merge` to finish.
|
|
222
|
+
|
|
223
|
+
### 7. Maintenance
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
gh-stack delete # remove a branch from the stack (re-parents children)
|
|
227
|
+
gh-stack split # cut a stack into two at a branch
|
|
228
|
+
gh-stack rename # rename a tracked branch (keeps metadata in sync)
|
|
229
|
+
gh-stack undo # restore from the last snapshot
|
|
230
|
+
gh-stack archive # manage archived stacks
|
|
231
|
+
gh-stack doctor # migrate & repair stack metadata
|
|
232
|
+
gh-stack update-prs # refresh stack visualization in PR descriptions
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Stack visualization
|
|
236
|
+
|
|
237
|
+
`submit` auto-maintains a `### 📚 Stacked on` footer in every PR body. **Do not
|
|
238
|
+
hand-write your own stack list** — it's regenerated each run, so a manual copy
|
|
239
|
+
just goes stale and contradicts the auto footer. Describe _this_ PR in its body;
|
|
240
|
+
let `submit` describe the stack. Link a specific sibling inline in prose if
|
|
241
|
+
needed.
|
|
242
|
+
|
|
243
|
+
## Safety model
|
|
244
|
+
|
|
245
|
+
- **Snapshots** before every destructive op (restack/sync/merge/remove); roll
|
|
246
|
+
back with `gh-stack undo`. Think of them as a reflog for stack operations.
|
|
247
|
+
- **Squash-merge safe** — uses temporary tags for stable rebasing (handles the
|
|
248
|
+
merge-base problem squash-merge creates).
|
|
249
|
+
- **Never force-pushes trunk.**
|
|
250
|
+
- **Metadata is local and never committed:** per-stack files under
|
|
251
|
+
`.git/.gh-stack/` plus git branch config. Inspect it via `gh-stack stacks
|
|
252
|
+
--json`, not by reading the store. (`gh-stack doctor` migrates the old v2
|
|
253
|
+
`.git/gh-stack-metadata.json` monolith.)
|
|
254
|
+
|
|
255
|
+
## Don'ts
|
|
256
|
+
|
|
257
|
+
- **Don't use `gt`/Graphite commands** — this is `gh-stack`.
|
|
258
|
+
- **Don't restack from the top** of the stack — restack flows upward.
|
|
259
|
+
- **Don't edit multiple branches then restack once** — finish each branch first.
|
|
260
|
+
- **Don't hand-write a stack section** in PR bodies — `submit` owns it.
|