mirror of
https://github.com/github/gh-stack.git
synced 2026-09-14 20:26:28 +08:00
Improve skill efficiency and performance (#388)
* break up skill into multiple files * better reordering instructions * updated branch placement instructions * clarify branch naming * recommend explicit arg for merge * refine troubleshooting text * address review comments
This commit is contained in:
+138
-846
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,179 @@
|
||||
# Command behavior
|
||||
|
||||
`gh stack <command> --help` is authoritative for flags and arguments. (`gh stack help <command>` only prints the top-level help.) This file only covers behavior `--help` does not
|
||||
explain: preconditions, side effects, atomicity, and failure modes.
|
||||
|
||||
## Contents
|
||||
|
||||
- [init](#init)
|
||||
- [add](#add)
|
||||
- [push](#push)
|
||||
- [submit](#submit)
|
||||
- [link](#link)
|
||||
- [sync](#sync)
|
||||
- [rebase](#rebase)
|
||||
- [view](#view)
|
||||
- [checkout](#checkout)
|
||||
- [unstack](#unstack)
|
||||
- [merge](#merge)
|
||||
- [Navigation](#navigation)
|
||||
|
||||
## init
|
||||
|
||||
Creates the stack and checks out the **last** branch in the list, so a single `init` can lay down
|
||||
the whole chain: `gh stack init auth api frontend`.
|
||||
|
||||
`init` processes branch arguments from bottom to top. Existing branches are adopted. If the first
|
||||
branch does not exist, it is created from the trunk; each later new branch is created from the
|
||||
branch immediately before it. There is no separate adopt mode — existence decides. `--base`
|
||||
selects a non-default trunk.
|
||||
|
||||
`init` also enables `git rerere`. Under a TTY the first run in a repo asks for confirmation; set
|
||||
`git config rerere.enabled true` beforehand to skip it.
|
||||
|
||||
## add
|
||||
|
||||
- **Must run from the top branch** of the stack (or the trunk when the stack is still empty).
|
||||
Anywhere else it exits **5** with `can only add branches on top of the stack`. Run `gh stack top`
|
||||
first.
|
||||
- **Uncommitted changes carry over.** Without `-Am`, `add` does not touch the working tree, so
|
||||
staged and unstaged changes follow you onto the new branch. Commit or stash first for a clean start.
|
||||
- **`add -Am` commits in place when the current branch has no commits yet** — for example
|
||||
immediately after `init` — instead of creating a branch. This is deliberate: the first layer
|
||||
usually needs its content before a second layer exists.
|
||||
- `-A` and `-u` are mutually exclusive, and both require `-m`.
|
||||
|
||||
## push
|
||||
|
||||
Pushes every active (non-merged, non-queued) branch in one multi-ref push with per-branch
|
||||
`--force-with-lease`.
|
||||
|
||||
**Not atomic.** Some branches may update while another is rejected. A rejection means that branch
|
||||
moved on the remote; fix that branch and rerun — rerunning is safe and skips what already landed.
|
||||
|
||||
`push` never creates or updates pull requests. Use `submit` for that.
|
||||
|
||||
## submit
|
||||
|
||||
Pushes each active branch, then creates a PR for every branch that lacks one, basing it on the
|
||||
first non-merged ancestor, then links them into a Stack on GitHub.
|
||||
|
||||
- **Not atomic.** Branches are pushed sequentially with per-branch `--force-with-lease`. If a later
|
||||
push is rejected, earlier pushes and PR updates stand. Fix the rejection and rerun the same command.
|
||||
- **A fully merged stack cannot be extended.** When every PR in the current stack is already merged,
|
||||
`submit` forks the remaining unmerged branches into a **new** stack rooted at the trunk and creates
|
||||
it on GitHub, leaving the merged stack untouched.
|
||||
- **Title generation with `--auto`:** a branch with a single commit uses that commit's subject as
|
||||
the title and its body as the PR body. A branch with multiple commits humanizes the branch name
|
||||
(hyphens and underscores become spaces). There is no flag for a custom title or body; use
|
||||
`gh pr edit` afterwards.
|
||||
- `--open` marks new *and existing* PRs ready for review; without it new PRs are drafts.
|
||||
- Requires stacked PRs to be enabled on the repository. If not, `submit` exits **9** when
|
||||
non-interactive (under a TTY it offers to create ordinary unstacked PRs instead).
|
||||
|
||||
## link
|
||||
|
||||
Creates or updates a stack on GitHub **without any local tracking state**. This is the path for
|
||||
branches managed by another tool or living in another worktree — see `troubleshooting.md`.
|
||||
|
||||
- Arguments are given bottom to top. Each is a branch name or a PR number; a numeric argument is
|
||||
tried as a PR number first and falls back to a branch name.
|
||||
- **A numeric first argument is treated as a stack number only when a stack with that number
|
||||
exists.** In that case the remaining arguments are appended to the top of that stack and you do
|
||||
not re-list its current PRs: `gh stack link 7 feature-c`. Arguments already in the stack are
|
||||
skipped; arguments belonging to a different stack are rejected.
|
||||
- Branch arguments are pushed automatically (non-force, atomic). Missing PRs are created with
|
||||
auto-generated titles and correctly chained bases; existing PRs with a wrong base are corrected.
|
||||
- Stack membership is **additive only** — `link` never removes a PR from a stack.
|
||||
|
||||
## sync
|
||||
|
||||
The routine command. Steps, in order:
|
||||
|
||||
1. **Fetch** from the remote.
|
||||
2. **Reconcile with the GitHub stack.** PRs added to the stack on github.com are pulled down and
|
||||
appended locally. On divergence, aborts when non-interactive (see `troubleshooting.md`).
|
||||
3. **Fast-forward the trunk.** Skipped when already current; warns when diverged.
|
||||
4. **Cascade rebase when needed.** This runs if the trunk moved, a stack branch was fast-forwarded
|
||||
from its remote, or a branch no longer contains its expected parent. Merged PRs are handled
|
||||
automatically. On conflict, **all branches are restored** to their pre-rebase state and the
|
||||
command exits **3**.
|
||||
5. **Push** all active branches, atomically.
|
||||
6. **Refresh PR state** from GitHub.
|
||||
7. **Sync the stack object** — link open PRs into a stack, additively. Only when two or more PRs
|
||||
exist. `sync` never opens PRs; that is `submit`.
|
||||
8. **Prune** local branches for merged PRs, only when `--prune` is passed in a non-interactive
|
||||
environment.
|
||||
|
||||
## rebase
|
||||
|
||||
Pulls from the remote and cascade-rebases. Use it when `sync` reported a conflict or when you need
|
||||
to rebase only part of the stack.
|
||||
|
||||
- `--upstack` rebases from the current branch to the top. This is what you run after editing a
|
||||
lower layer.
|
||||
- `--downstack` rebases from the trunk to the current branch.
|
||||
- `--no-trunk` skips fetching and the trunk rebase entirely, aligning stack branches with each
|
||||
other only.
|
||||
- `--continue` after staging resolutions; `--abort` restores every branch.
|
||||
- A merged PR is detected automatically and replayed with `--onto` against the correct target, so a
|
||||
squash-merged parent does not produce spurious conflicts.
|
||||
- Starting a rebase while one is in progress exits **7**.
|
||||
|
||||
## view
|
||||
|
||||
- `--json` writes the machine-readable payload to stdout. Its schema is in `SKILL.md`.
|
||||
- Bare `view` opens a full-screen TUI when stdout is a TTY, and prints static text when piped.
|
||||
- `--short` prints a compact one-line-per-branch summary and never opens the TUI, but it is
|
||||
formatted for humans; parse `--json` instead.
|
||||
- `view` refreshes PR state from GitHub as a side effect, best-effort — it does not fail when the
|
||||
API is unreachable.
|
||||
|
||||
## checkout
|
||||
|
||||
Accepts a stack number, PR number, PR URL, or branch name.
|
||||
|
||||
- A bare number resolves as a **stack number first**, then a PR number, then a branch name.
|
||||
- Stack numbers, PR numbers, and PR URLs fetch from GitHub, pull the branches down, and set the
|
||||
stack up locally.
|
||||
- A **branch name resolves against locally tracked stacks only** and never contacts GitHub. Use a
|
||||
stack or PR number to pull a stack that is not tracked locally.
|
||||
- If a local stack already exists over those branches with a different composition, `checkout`
|
||||
cannot be forced past it. Run `gh stack unstack --local` first, then retry.
|
||||
- `checkout` has no flags. It relies on `remote.pushDefault` when several remotes exist.
|
||||
|
||||
## unstack
|
||||
|
||||
Removes the stack **grouping** only. It never deletes pull requests or branches.
|
||||
|
||||
- With no argument it targets the active stack — the one containing the current branch — removing
|
||||
it on GitHub and locally.
|
||||
- With a stack number it works from anywhere in the repository, tracked locally or not, via the API.
|
||||
Local tracking is also removed when present.
|
||||
- `--local` removes local tracking only and never contacts GitHub. Combining `--local` with a stack
|
||||
number that is not tracked locally is an error.
|
||||
- An unknown stack number exits **2**.
|
||||
|
||||
## merge
|
||||
|
||||
- Scope with an argument: pass a PR number to merge that PR and every unmerged PR below it in the
|
||||
stack, or pass a stack number to merge every unmerged PR in that stack.
|
||||
- **All-or-nothing.** If any PR in that exact merge set cannot be merged, none are, and the reason
|
||||
is reported.
|
||||
- The method comes from `--squash`, `--rebase`, `--merge`, or `--merge-method <method>`. Without
|
||||
one, the last-used method is reused.
|
||||
- Only basic PR state is checked before merging: open and not a draft. Bypassing merge requirements
|
||||
is not supported for stacks.
|
||||
- **A merge queue on the base branch overrides everything.** The stack is added to the queue rather
|
||||
than merged; the queue chooses the method and any method flag you passed is ignored with a
|
||||
warning. Queued PRs are submitted together but land as the queue processes them, so they may merge
|
||||
in separate groups rather than all at once.
|
||||
- `gh pr merge` cannot merge a stack. Always use `gh stack merge`.
|
||||
|
||||
## Navigation
|
||||
|
||||
`up`, `down`, `top`, `bottom`, and `trunk` are always non-interactive. `up` and `down` accept a
|
||||
count (`gh stack up 3`). Movement clamps at the stack bounds, and merged branches are skipped when
|
||||
navigating from an active branch, so `bottom` lands on the lowest *unmerged* branch.
|
||||
|
||||
`gh stack switch` is a selection menu with no non-interactive path. Use the commands above instead.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Designing a stack
|
||||
|
||||
How to decide what goes in each layer. Read this before running `gh stack init`.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Plan the layers before writing code](#plan-the-layers-before-writing-code)
|
||||
- [Branch naming](#branch-naming)
|
||||
- [Staging changes deliberately](#staging-changes-deliberately)
|
||||
- [When to add a layer](#when-to-add-a-layer)
|
||||
- [One stack, one story](#one-stack-one-story)
|
||||
|
||||
## Plan the layers before writing code
|
||||
|
||||
A stack is a dependency chain. If code in one layer depends on code in another, the dependency must
|
||||
live in the same branch or a lower one. That constraint is much cheaper to satisfy by planning than
|
||||
by restructuring later, because there is no non-interactive in-place reorder — fixing the order
|
||||
means`unstack` and `init` again.
|
||||
|
||||
Decide the layers first, then write code into them:
|
||||
|
||||
```
|
||||
(main) <- todo-app/models <- todo-app/api <- todo-app/frontend <- todo-app/integration
|
||||
```
|
||||
|
||||
- `todo-app/models` — shared types and schema
|
||||
- `todo-app/api` — routes that use the models
|
||||
- `todo-app/frontend` — components that call the routes
|
||||
- `todo-app/integration` — tests exercising the whole feature
|
||||
|
||||
This is illustrative. Infer the stack topic and layer names from the actual task; do not reuse
|
||||
`todo-app` or these layer names literally.
|
||||
|
||||
The failure mode to avoid is writing everything on one branch and trying to split it afterwards.
|
||||
If a task is large enough to warrant a stack, create the stack at the start.
|
||||
|
||||
## Branch naming
|
||||
|
||||
Prefer a shared topic prefix plus the layer's concern:
|
||||
`<topic>/<concern>` — for example, `billing/schema`, `billing/api`, `billing/ui`.
|
||||
This keeps related branches recognizable without using generic names that could belong to any
|
||||
stack. **User and repository branch naming conventions take precedence; follow them instead.**
|
||||
|
||||
Names are used exactly as given — nothing is prepended or transformed, and slashes are kept, so
|
||||
`gh stack add refactor/foo` creates a branch literally named `refactor/foo`.
|
||||
|
||||
If you pass `-m` without a branch name, the name is generated from the commit message in
|
||||
date-and-slug form (for example `03-24-add_api_routes`). Prefer naming the branch yourself.
|
||||
|
||||
## Staging changes deliberately
|
||||
|
||||
Use `git add` and `git commit` directly rather than the `add -Am` shortcut. The point is control
|
||||
over which changes land in which branch. With several modified files in the working tree, stage the
|
||||
subset that belongs to the current layer, commit it, then create the next branch and stage the rest
|
||||
there:
|
||||
|
||||
```bash
|
||||
git add internal/models/user.go internal/models/session.go
|
||||
git commit -m "Add user and session models"
|
||||
|
||||
gh stack add api-routes
|
||||
git add internal/api/routes.go internal/api/handlers.go
|
||||
git commit -m "Add user API routes"
|
||||
```
|
||||
|
||||
Multiple commits per branch are fine. What matters is that every commit in a branch serves the same
|
||||
concern, and that a change belonging to a different concern goes in a different branch.
|
||||
|
||||
Note that `gh stack add <branch>` without `-Am` does not touch the working tree, so uncommitted
|
||||
changes carry over to the new branch. Commit or stash first if you want the new layer to start clean.
|
||||
|
||||
## When to add a layer
|
||||
|
||||
Add a branch when you start a **different concern that depends on what you have built so far**.
|
||||
Signals:
|
||||
|
||||
- Moving from backend to frontend, or from core logic to tests or documentation
|
||||
- The next changes have a different reviewer audience
|
||||
- The current branch's diff is already large enough to review on its own
|
||||
|
||||
A layer that cannot be described in one sentence is usually two layers.
|
||||
|
||||
## One stack, one story
|
||||
|
||||
A stack should read as a coherent progression: a reviewer walks the PRs bottom to top and sees the
|
||||
feature being built.
|
||||
|
||||
**Use a single stack** when every branch serves the same feature or project, even if the layers span
|
||||
different concerns.
|
||||
|
||||
**Start a separate stack** for unrelated work — a different feature, an unrelated bug fix, an
|
||||
independent refactor. Do not mix efforts into one stack just because you happened to work on both.
|
||||
Use `gh stack init` for the new effort, or `gh stack checkout <target>` to move between existing
|
||||
stacks.
|
||||
|
||||
A trivial incidental fix can ride along in the current stack. Once it grows into its own project, it
|
||||
deserves its own stack.
|
||||
@@ -0,0 +1,159 @@
|
||||
# Troubleshooting and recovery
|
||||
|
||||
## Contents
|
||||
|
||||
- [Rebase conflicts (exit 3)](#rebase-conflicts-exit-3)
|
||||
- [After a squash merge](#after-a-squash-merge)
|
||||
- [Local and remote stacks have diverged](#local-and-remote-stacks-have-diverged)
|
||||
- [Restructuring a stack](#restructuring-a-stack)
|
||||
- [Branch belongs to several stacks (exit 6)](#branch-belongs-to-several-stacks-exit-6)
|
||||
- [Driving stacks from another tool or worktree](#driving-stacks-from-another-tool-or-worktree)
|
||||
- [Stack file is locked (exit 8)](#stack-file-is-locked-exit-8)
|
||||
- [An interrupted modify session (exit 10)](#an-interrupted-modify-session-exit-10)
|
||||
|
||||
## Rebase conflicts (exit 3)
|
||||
|
||||
`rebase` and `sync` both exit 3 on conflict. `sync` restores every branch to its pre-rebase state
|
||||
first, so a failed `sync` leaves nothing half-applied; a failed `rebase` stops mid-flight and waits.
|
||||
|
||||
```bash
|
||||
gh stack rebase
|
||||
# exit 3 — conflicted paths are listed on stderr
|
||||
git add <resolved paths>
|
||||
gh stack rebase --continue # repeat if the next branch also conflicts
|
||||
```
|
||||
|
||||
`gh stack rebase --abort` restores every branch in the stack, not just the current one.
|
||||
|
||||
Because `init` enables `git rerere`, a conflict you resolve once is replayed automatically the next
|
||||
time the same conflict appears — which is common, since a change low in the stack is rebased through
|
||||
every branch above it. Without `rerere`, repeated conflicts may need manual resolution on each
|
||||
affected layer.
|
||||
|
||||
## After a squash merge
|
||||
|
||||
A squash merge replaces the branch's commits with one new commit, so the originals no longer exist
|
||||
in the trunk's history and an ordinary rebase would try to replay them again.
|
||||
|
||||
`gh stack sync` detects this and rebases with `--onto` against the correct target, skipping the
|
||||
merged branch:
|
||||
|
||||
```bash
|
||||
gh stack sync
|
||||
gh stack view --json # merged branch reports "isMerged": true, "state": "MERGED"
|
||||
```
|
||||
|
||||
No manual action is needed. If the replay conflicts, `sync` restores all branches and exits 3.
|
||||
Run `gh stack rebase` to rerun the rebase, which will stop at the conflict and allow you to resolve
|
||||
and then `--continue` until complete. Use `gh stack sync --prune` to also delete local branches for
|
||||
merged PRs.
|
||||
|
||||
## Local and remote stacks have diverged
|
||||
|
||||
Divergence means the local stack and the stack on GitHub changed in different ways — for example
|
||||
branches were added locally while a PR was added to the stack on github.com.
|
||||
|
||||
When non-interactive, `sync` prints both chains, changes nothing, and exits **0** with
|
||||
`Sync aborted`. Success here does not mean the sync happened; check for that message, or re-run
|
||||
`gh stack view --json` and compare.
|
||||
|
||||
Two resolution paths:
|
||||
|
||||
- **Keep the remote version.** Drop local tracking and pull the stack back down.
|
||||
|
||||
```bash
|
||||
gh stack unstack --local # keeps the stack on GitHub
|
||||
gh stack checkout <stack-number> # or a PR number
|
||||
```
|
||||
|
||||
- **Keep the local version.** Remove the grouping on GitHub, then recreate it from local state.
|
||||
|
||||
```bash
|
||||
gh stack unstack # removes the grouping; PRs and branches survive
|
||||
gh stack submit --auto
|
||||
```
|
||||
|
||||
Neither path deletes pull requests or branches.
|
||||
Remote unstacking leaves PRs that are merging (auto-merge enabled) or are queued (in a merge queue)
|
||||
stacked. If needed, clear that state before retrying.
|
||||
|
||||
## Restructuring a stack
|
||||
|
||||
There is no non-interactive reorder, rename, or removal. `add` run from the wrong branch suggests
|
||||
`gh stack modify`, but that is TUI-only. Tear the stack down and rebuild it instead:
|
||||
|
||||
```bash
|
||||
gh stack unstack # removes local tracking and the GitHub grouping
|
||||
# Rename or drop branches, and rewrite ancestry as needed.
|
||||
gh stack init --base main branch-1 branch-2 branch-3
|
||||
gh stack submit --auto # re-link on GitHub
|
||||
```
|
||||
|
||||
`init` adopts branches that already exist, so the rebuild reuses them rather than creating new ones.
|
||||
Existing PRs survive. Once Git ancestry is correct, `submit` updates their base branches and
|
||||
re-links the stack on GitHub.
|
||||
|
||||
Changing metadata does **not** change Git ancestry. Reorder commits first, then rebuild the stack.
|
||||
For example, to change `main <- models <- migration <- ui` into
|
||||
`main <- migration <- models <- ui`:
|
||||
|
||||
```bash
|
||||
old_models=$(git rev-parse models)
|
||||
old_migration=$(git rev-parse migration)
|
||||
git rebase --onto main "$old_models" migration
|
||||
git rebase --onto migration main models
|
||||
git rebase --onto models "$old_migration" ui
|
||||
gh stack unstack
|
||||
gh stack init --base main migration models ui
|
||||
```
|
||||
|
||||
The first rebase moves migration-only commits onto trunk, the second replays model commits above
|
||||
them, and the third replays UI-only commits above models. Preserve the old boundary SHAs before
|
||||
moving any branch. For a different reorder, identify each layer's range with
|
||||
`git log <old-parent>..<branch>`, then replay the ranges bottom to top.
|
||||
|
||||
## Branch belongs to several stacks (exit 6)
|
||||
|
||||
Commands exit 6 when the current branch cannot identify a single stack — typically because it is the
|
||||
trunk of more than one stack. There is no flag to disambiguate.
|
||||
|
||||
```bash
|
||||
gh stack checkout <a-branch-unique-to-the-intended-stack>
|
||||
```
|
||||
|
||||
Then rerun. Commands that take an explicit stack number (`merge 7`, `unstack 7`) sidestep the
|
||||
problem entirely, since they do not infer the stack from the current branch.
|
||||
|
||||
## Driving stacks from another tool or worktree
|
||||
|
||||
`gh stack link` creates and updates stacks purely through the API, with no local tracking state.
|
||||
Use it when branches are managed by jj, Sapling, git-town, a separate worktree, or any workflow
|
||||
where the local `.git/gh-stack` file would be wrong or absent.
|
||||
|
||||
```bash
|
||||
gh stack link branch-a branch-b branch-c # bottom to top
|
||||
gh stack link --base develop --open a b c # non-default trunk, ready for review
|
||||
gh stack link 10 20 30 # by PR number
|
||||
gh stack link 7 feature-d # append to existing stack #7
|
||||
```
|
||||
|
||||
Because `link` writes no local state, the local navigation commands (`up`, `down`, `top`, `bottom`)
|
||||
will not work on the result. Use `gh stack checkout <stack-number>` if you later want local tracking.
|
||||
|
||||
## Stack file is locked (exit 8)
|
||||
|
||||
Another `gh stack` process holds the exclusive lock on `.git/gh-stack.lock`. The lock times out
|
||||
after about five seconds, so wait and retry. A persistent exit 8 means another process still holds
|
||||
the lock; identify and stop that process before retrying.
|
||||
|
||||
## An interrupted modify session (exit 10)
|
||||
|
||||
`gh stack modify` is TUI-only and should never be invoked by an agent. If a repository is left in
|
||||
this state by someone else, restore it:
|
||||
|
||||
```bash
|
||||
gh stack modify --abort
|
||||
```
|
||||
|
||||
Related: `submit` also detects a pending modify state, and under a TTY asks before overwriting the
|
||||
stack on GitHub with local state.
|
||||
Reference in New Issue
Block a user