diff --git a/AGENTS.md b/AGENTS.md index 046588f..1045b8b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -622,6 +622,102 @@ after every merge. - New dev-only files belong in `.gitattributes` as `export-ignore` so they stay out of consumer installs. +### Stacking with `gh stack` + +**Stack whenever the work divides into portions a human could review separately.** The size cap +above is the floor, not the test: a change that fits in ten files still gets split if a reviewer +would have to hold two unrelated arguments in their head to sign it off. The unit is a decision a +reviewer can accept or reject on its own — one hook wired, one collaborator extracted, one +invariant enforced — with its own tests and its own `Why this way` block. A reviewer who can finish +a PR in one sitting reads it; a reviewer facing everything at once skims it, and skimming is how a +wrong decision reaches `main` with an approval on it. + +What that rules out is splitting for its own sake. Each branch has to leave `main` releasable, so a +portion that cannot compile, pass the suite or be described in one `What` line is not a portion — +it belongs with the branch that completes it. When the work genuinely is one decision, one PR is +the right answer and a stack of one is nothing but ceremony. + +**Every PR in a stack is titled ` [X/Y]: `** — so +`Conflict handling [2/4]: resolve the standalone conflict` sits between the `[1/4]` and the `[3/4]` +of the same prefix. The prefix names the work all of them belong to and `X/Y` names the position and +the size, which is the whole of what a reviewer scanning the PR list needs: that these belong +together, which one to open first, and how much more is coming. `Y` is a count, so appending a +branch renumbers every PR already open — retitle them in the same pass as the `gh stack add`, or the +numbering says the series is shorter than it is. Nothing about the prefix survives `gh stack submit +--auto`, which titles a single-commit branch with that commit's subject and a multi-commit branch +with its own name, hyphens and underscores turned into spaces. There is no flag for a title, so set +it with `gh pr edit --title` after submitting; a later submit leaves what is already there alone. + +**A PR based on another branch is not automatically a stack.** The commonest reason to cut from an +open branch instead of `main` is that both touch the same file and cutting from `main` would +guarantee a conflict — that is a base branch and nothing more. Point the PR's base at the branch +below it and stop; do not run `gh stack init`. A Stack on GitHub is a claim to the reviewer that the +PRs are one piece of work meant to be read in order, and making that claim about two changes that +merely share a file sends them looking for a through-line that was never there. Reach for the +tooling below only when the ordering is the argument. + +The stack is managed with the **`github/gh-stack`** extension, invoked `gh stack`. Do not hand-roll +the stack with merges: `rebase` and `submit` force-update the branches the stack owns, which is what +keeps each PR's diff to its own commits. Four operations cover the whole workflow, and none of them +are guessable from `--help`, so use these verbatim. + +**Adopt existing branches into a stack**, bottom to top, naming the trunk with `--base` so the trunk +gets no PR of its own: + +```bash +gh stack init --base main 34-first 35-second 36-third +gh stack view --json # confirm the order before submitting +``` + +`view` takes `--json` because a bare `gh stack view` is the interactive TUI; `--short` is the +one-line-per-branch form for a human reading along. + +`--base` is whatever the bottom branch cut from. That is `main` for an ordinary series — and it is +the **tip branch of the lower stack** when a second stack is built on top of one that is still open, +which is the only way to keep the lower stack's PRs out of the new one. + +**Append one already-existing branch** to a stack that exists. `gh stack init` refuses this with +"already exists in a stack", and `gh stack add` is what takes it: a name that is already a branch in +git is adopted, and only a name that is not gets created, which is the same rule `init` follows. +`add` has to run from the **top** of the stack, so get there with `gh stack top` rather than by +naming a branch — anywhere else it exits `5` with "can only add branches on top of the stack": + +```bash +gh stack top +gh stack add 37-fourth +``` + +`add` leaves the working tree alone, so uncommitted changes follow you onto the branch it checks +out; commit or stash first. `gh stack modify` also adds branches, but it is an interactive TUI, so +it cannot be driven from a script or by an agent. + +**Propagate an edit made low in the stack** by cascading rebase, never by merging the lower branch +upward: commit the edit on the lower branch, check out the **lowest changed** branch, and rebase +that branch and everything above it: + +```bash +git checkout 35-second +gh stack rebase --upstack --preserve-dates +``` + +`--preserve-dates` — the alias for `--committer-date-is-author-date` — goes on *every* rebase, not +just this one. Without it each rebase restamps the committer date of every commit it rewrites, and +a stack is rebased repeatedly, so the whole history walks forward to whenever the last submit +happened. + +`--upstack` is the other half: a rebase with no scope flag re-creates *every* commit in the stack, so +the submit after it force-pushes lower branches nothing changed on. Take the full-stack form only +when the trunk moved or the bottom branch itself did — and with the same flag: + +```bash +gh stack rebase --preserve-dates +``` + +**Publish** with `gh stack submit --auto`: it pushes every branch, force where the rebase made that +necessary, and links them as a Stack on GitHub. It preserves existing PR titles, bodies and draft +state, so it is safe to re-run; `--open` is the flag that marks them ready for review. A +non-interactive run implies `--auto`. + ## Known, and deliberately not fixed in 1.0.0 **`Activator::maybe_run()` can double-run under concurrency.** It reads the option, runs the diff --git a/cspell.json b/cspell.json index 1777d7e..b4c52d8 100644 --- a/cspell.json +++ b/cspell.json @@ -72,6 +72,7 @@ "unwiring", "uopz", "upgrader", + "upstack", "vlucas", "worktree", "wpautop",