I keep finding git behaviours the same way: set up the case, run the command, then check the state I actually cared about instead of trusting the success message. Guides pair rerere.enabled with rerere.autoupdate when the second one removes the checkpoint the first one earns you. GitHub reports mergeable while the reviewer is still reading stale bytes. --autosquash prints Successfully rebased and updated and leaves a fixup! commit sitting in history.
Copy the config. The reasoning is why you would leave one of them off.
This started as a comment on Sylwia Laskowska's git list. She asked me to turn it into a post. The first four items are from ordinary rebasing. The rest I measured this week on a stacked-PR repo.
1. rerere does not learn from git add alone
git config rerere.enabled true
rerere is reuse recorded resolution. It remembers how you resolved a conflict and replays it when the same one appears. On a long rebase that is resolving something once instead of five times.
CONFLICT (content): Merge conflict in f.txt
Staged 'f.txt' using previous resolution.
Resolve, git add, then abort immediately, and it recorded nothing. The next attempt conflicts identically. That bit me twice before I understood it.
Two things are going on and my test could not separate them, so here are both. git add does not record the postimage — git commit, git rebase --continue, or an explicit git rerere do. And git rebase --abort runs rerere clear, which wipes the in-flight metadata anyway.
What I could measure, on git 2.39.5: resolve + git add + abort, and the next attempt conflicts identically. Resolve + git add + explicit git rerere + abort, and it replays — before the rebase goes anywhere. So "it learns when the rebase finishes" is the wrong model either way.
2. It matches on the normalized conflict hunk, not the pathname
I taught it a resolution in one.txt. Then I created the same conflicting hunk in a completely different file, two.txt, on a different branch. It replayed the one.txt answer into two.txt.
On a rebase that is usually what you want. It is not always what you want. If the right answer differs between those two places, it will quietly give you the first one.
3. Which is why I would not turn on rerere.autoupdate
I kept seeing them paired. I checked the index in both modes:
autoupdate off -> UU two.txt still conflicted, you must read it and git add
autoupdate on -> M two.txt fully staged, nothing asks you to look
Autoupdate off is the checkpoint that catches a replay you did not want. Leave it off and rerere still writes the resolution into the file. You just have to look at it before adding.
If it got it wrong:
git rerere forget <path> # drop what it learned
git checkout -m <path> # bring the conflict markers back
4. --autosquash can succeed and still leave the fixup in history
git commit --fixup abc1234
git rebase -i --autosquash abc1234~1
First marks it. Second reorders and squashes it. The todo list opens already correct.
Two sharp edges.
Bare git rebase -i --autosquash with no range fails outright on a branch with no upstream, so you need the base.
And if the range does not reach far enough back to include the target, it does not warn you. It prints Successfully rebased and updated and leaves the fixup! commit sitting in your history.
git log --oneline | grep fixup!
Worth running after. The rebase step rewrites history — git commit --fixup just adds a commit — so the usual rule about rebasing shared commits applies to the second line, not the first.
5. Retargeting a pull request changes what it compares against. It does not update the head branch.
This one I did not set up on purpose. I had a stack: main ← #3 ← #4 ← #2. After #3 and #4 merged, I retargeted #2 onto main. GitHub immediately showed base: main, mergeable, clean.
The new base commit was still not an ancestor of my head.
I measured it. Head b1cf109 versus main at e6dc136: GitHub compare status diverged, behind by 8. After git merge origin/main (commit b4ac271): behind by 0.
git fetch origin main
git merge-base --is-ancestor origin/main HEAD && echo "head contains main" || echo "STALE — merge first"
Exit 1 is the stale case. GitHub's "Change base" button is not "Update branch." Change base retargets what the PR is compared against. Update branch, or git merge origin/main, is what actually brings the commits in.
Those SHAs are from a private stack, so this one is a field note rather than something you can clone and rerun. The check itself is two lines and works anywhere.
That head was what the automated reviewer had to work from. It carried an old implementation I had already replaced on main, and I burned a cycle chasing a finding about code that only still existed on that stale branch — before I thought to check whether the branch contained what I thought it contained.
The fix is one merge, then the ancestor check. MERGEABLE does not mean "this head contains current main."
6. Pin evidence links to a commit, not to main
A link to /blob/main/path/file shows whatever main says today, and 404s outright if the path later moves. A link to /blob/<sha>/path/file is immutable. Both may return 200 right now. Only one of them still means the same thing next month.
I hit this the same week. main still carried an old write-up while the corrected file only existed on a branch. I published the SHA-pinned URL so a reader could open the bytes I was citing, not whatever landed on default later.
If you are citing evidence, cite the sha.
7. git worktree instead of stashing, if you have untracked files you cannot lose
Stash does not save untracked files unless you remember -u. I had an untracked scratch file I needed to keep through a week of branch hops. Worktrees solved it without the dance: each branch in its own directory, one object store, untracked files stay put.
git worktree add -b some/branch /tmp/scratch origin/main
When you are done: git worktree remove. If you have ever needed to fix one branch while another is mid-edit, this is the command.
Same disease as the rest of the list, incidentally. git stash reports success and your untracked file is simply not in it.
The method
--autosquash prints success while leaving a fixup! behind. A retargeted PR reports MERGEABLE while its head is eight commits behind the branch it now claims as base. rerere.autoupdate stages the replay cleanly and removes the unmerged path you would otherwise have been forced to look at.
In each case the message was true and the state was not what I assumed. And each one needed a different thing checked: history for the leftover fixup!, ancestry for the stale head, the index for UU versus M.
Which is the actual lesson, and it is not "check the index":
A success message tells you the command completed according to its own contract. It does not tell you the repository now satisfies the condition you actually cared about. Those are different sentences. Go check the one you cared about.
Measured on git 2.39.5 locally; the GitHub compare behaviour is as of this week. Items 1–4 are reproducible on any repo in about two minutes. Item 5 is a field note from a private stack.
This post exists because Sylwia Laskowska wrote a git list good enough that I went and tested the rebase section against it, and then told me the comment should be its own post. She was right, and I would not have written it otherwise. Go read hers.
Top comments (1)
Wow! These are some great items. Curious, did you share them on GitHub and report? I am just curious, do you think this behavior may exist on Gitlabs too? I mean some of us, well it’s me, take this as granted to be mostly working. My company has close connections with Girhub through enterprise agreements and we report issue occasionally. This is a treasure trove of things you are finding where I didn’t expect to see these issues. Thanks again for finding and reporting them