chore(claude): harden the rebase-onto-main skill against losing work (#28368)

The skill force-pushes over the only copy of the pre-rebase head without
ever checking that the rebase preserved it, and says nothing about the two
things that most often go wrong around it.

Adds a git range-diff gate before the push, the worktree collision that
makes git checkout fail mid-loop, and the --onto follow-up a stacked child
branch needs once its base moves.

Assisted-by: Claude:claude-opus-5

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
This commit is contained in:
Jacob Dahl
2026-08-24 17:18:15 -06:00
committed by GitHub
parent 8c1dc380cf
commit af2e7b4311

View File

@@ -17,6 +17,16 @@ When a parent branch is squash-merged, its individual commits become a single ne
1. **Identify the branch.** Use `$ARGUMENTS` if provided, otherwise use the current branch. 1. **Identify the branch.** Use `$ARGUMENTS` if provided, otherwise use the current branch.
Check `git worktree list` first. A branch checked out in another worktree cannot be checked out here — `git checkout` fails with `fatal: '<branch>' is already used by worktree at ...`. Rebase it in place instead: `git -C <worktree-path> rebase main`.
When rebasing several branches in a loop, test the checkout's exit status explicitly:
```
git checkout -q "$b" || { echo "SKIP $b (worktree?)"; continue; }
```
Without that guard the loop rebases whatever is *currently* checked out, once per failed iteration, and reports success — the rebase really did work, just on the wrong branch. `set -e` is not a substitute: it is ignored in a Claude Code Bash tool call (it only takes effect when bash runs a script file).
2. **Fetch and update main:** 2. **Fetch and update main:**
``` ```
git fetch origin main:main git fetch origin main:main
@@ -62,10 +72,29 @@ When a parent branch is squash-merged, its individual commits become a single ne
``` ```
Confirm only the expected commits are present. Confirm only the expected commits are present.
Then prove no content changed, because the next step overwrites the only copy of the old head:
```
git range-diff <old-merge-base>..<old-head> main..<branch>
```
Every line must be marked `=` (identical patch). `!` means a commit's diff changed, `<` means one was dropped, `>` means one appeared. Anything but `=` is a rebase that lost or altered work — stop and look before pushing. Scripted:
```
rd=$(git range-diff --no-color <old-merge-base>..<old-head> main..<branch>)
tot=$(echo "$rd" | grep -cE '^ *[0-9]+:')
eq=$(echo "$rd" | grep -cE '^ *[0-9]+: +[0-9a-f]+ = ')
[ "$tot" = "$eq" ] && echo CLEAN || echo REVIEW
```
11. **Ask the user** before force-pushing. When approved: 11. **Ask the user** before force-pushing. When approved:
``` ```
git push origin <branch> --force-with-lease git push origin <branch> --force-with-lease
``` ```
Use `--force-with-lease`, never a bare `--force`. Push to the remote that holds the branch's PR head.
If this branch is the base of a stacked PR, the branch above it still points at the pre-rebase commits. Rebase it onto the new head and force-push it too:
```
git rebase --onto <branch> <old-head-of-branch> <branch-above>
```
Skipping this makes the upper PR show every commit on `main`.
12. **Clean up** the old branch: 12. **Clean up** the old branch:
``` ```