You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 7888c76
Browse filesBrowse the repository at this point in the historyBrowse files
docs: correct the stacked-PR guidance for a rebase-only repository
`main` keeps a linear history now, so the repository allows rebase-and-merge
only — squash and merge-commit are both disabled. Three pieces of guidance
were wrong under that, and two of them fail quietly.
"Use merge-commit, not squash, for a stack" recommended `gh pr merge --merge`,
which now errors. The advice it carried is also unavailable rather than merely
unfashionable: rebase replays a branch onto main as new commits, so every
child of a merged branch has diverged history and must be rebased and
force-pushed between merges. That is the expensive path, and there is no
cheaper one to prefer.
The stack-health check was unsound. `git merge-base --is-ancestor` asks
whether the tip's commits are ancestors of the branch that absorbed them, and
after a rebase merge they never are — it reports STRANDED for a stack that
landed perfectly. Replaced with a content diff, and the healthy state named
explicitly, because an empty diff over differing SHAs looks alarming and is
not.
Records two things measured while landing #153 and #154: branch protection is
strict, so the next PR reads `mergeable_state: "behind"` until rebased, and
`rebaseable` reads `null` for a while after each merge while GitHub recomputes
— reading that null as "not mergeable" is what stranded commits mid-stack
before.
Also records that GitHub does not re-sign what it rebases, so every commit on
main reports `N` despite `-S` being mandatory locally. Worth knowing in both
directions: on main it is expected, and before main it means the flag was
missed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cyga14bww8rmazH2XrF8ms
Copy file name to clipboardExpand all lines: CLAUDE.md
+44-5Lines changed: 44 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -147,21 +147,60 @@ Merge each PR **down** into its parent's branch, from the tip to the bottom:
147
147
- Bring the bottom branch up to date with `main`, let `Validate` pass, then do the **single** protected merge to `main`.
148
148
- Result: one CI cycle instead of N, and every PR gets a real **Merged** badge (not "closed/absorbed").
149
149
150
-
**Merge the down-merges one at a time, not in a loop.** Merging a child immediately invalidates the parent PR's mergeability (`gh pr merge` fails with "Pull Request is not mergeable") until GitHub recomputes. In a tight loop this makes merges land **out of order**, which strands the tip's commits part-way down the stack (e.g. `skill`/`eval` never propagate past `help`). Merge each PR, wait a few seconds, confirm the next is `MERGEABLE`, then continue. After the down-merges, verify the bottom branch actually contains the tip before the final merge:
150
+
**Merge the down-merges one at a time, not in a loop.** Merging a child immediately invalidates the parent PR's mergeability until GitHub recomputes — `gh pr merge` fails with "Pull Request is not mergeable", and the API reports `rebaseable: null`. In a tight loop this makes merges land **out of order**, which strands the tip's commits part-way down the stack (e.g. `skill`/`eval` never propagate past `help`). Merge each PR, wait for the next to report a boolean `rebaseable`, then continue.
151
+
152
+
**Verify by content, not by ancestry.** Rebase-and-merge replays commits under new SHAs, so the tip's original commits are never ancestors of the branch that absorbed them, and the obvious check reports a false `STRANDED`:
If stranded, reconcile from the tip (a tip branch contains the entire stack): on the bottom branch, `git merge origin/main` then `git merge origin/<tip-branch>`, confirm the only diff vs. the tip is whatever landed on `main` separately, and push.
162
+
An empty diff with differing SHAs is the *expected* healthy state after a rebase merge, not evidence of a problem. If the diff is genuinely non-empty, reconcile from the tip — a tip branch contains the whole stack — then re-check the diff and push.
157
163
158
164
### Never `--delete-branch` mid-stack
159
165
160
166
`gh pr merge <n> --delete-branch` on a stacked PR **closes the child** PR (its base branch vanishes) instead of retargeting it. Leave branches in place during the stack; clean them up only after the whole stack has landed.
161
167
162
-
### Use merge-commit, not squash, for a stack
168
+
### Rebase is the only merge method, and a stack pays for it
169
+
170
+
`main` keeps a linear history, so the repository allows **rebase-and-merge only** — squash and merge-commit are both disabled. Confirm rather than assume, since this changed:
171
+
172
+
```bash
173
+
gh api repos/{owner}/{repo} --jq '"squash=\(.allow_squash_merge) merge=\(.allow_merge_commit) rebase=\(.allow_rebase_merge)"'
174
+
# squash=false merge=false rebase=true
175
+
```
176
+
177
+
`gh pr merge --merge` and `--squash` both fail. Use `gh pr merge <n> --rebase`.
178
+
179
+
**This is the expensive case for a stack, and there is no cheaper option available.** Rebase-and-merge replays the branch onto `main` as *new commits with new SHAs*. Every child then contains the pre-rebase versions of its ancestors' commits, so the child is not merely behind — its history diverged. After each merge you must rebase the next branch onto the updated `main` and force-push it. The old guidance to prefer merge-commits so children stay clean no longer applies; that door is closed.
-**Branch protection is `strict_up_to_date: true`,** so the next PR reports `mergeable_state: "behind"` until you rebase it. That is not a conflict; it is the protection asking for the rebase you owe it.
193
+
-**Right after a merge, `rebaseable` reads `null`** while GitHub recomputes. Poll until it is a boolean rather than treating `null` as "not mergeable" — reading it as a failure is what stranded commits mid-stack before.
194
+
195
+
### Rebase-and-merge lands unsigned commits on `main`
196
+
197
+
Commits are signed locally (`git commit -S`, mandatory above), but **GitHub rewrites them when it rebases, and does not re-sign.** Every commit on `main` reports `N`:
198
+
199
+
```bash
200
+
git log --format='%G? %h %s' -5 origin/main # N, N, N, …
201
+
```
163
202
164
-
Stacked branches share commits (each child contains its ancestors). Prefer explicit `gh pr merge --merge` — `--merge` keeps children clean, while squash rewrites the parent into a new commit the children don't have, forcing a manual `git merge origin/main` reconciliation on every child between merges and inviting phantom conflicts.
203
+
Nothing is wrong and nothing needs fixing on `main`. Know it so that `%G?` on a merged commit is not mistaken for a signing failure, and so a fresh commit reading `N`**before** it reaches `main` is recognised as the real problem it is — that one means `-S` was missed.
165
204
166
205
### Recovery if a child PR gets closed by base-branch deletion
0 commit comments