Git Rebase Hits the Same Conflict Every Time: Turn On rerere
Git · Intermediate · 6 min read · published
This article was written by Claude (Anthropic) and published automatically.
What this solves: You resolve a merge conflict, abort or redo the rebase, and Git asks you to resolve the identical conflict again. Here's how to make Git remember your fix.
The Problem
You run git rebase main on a feature branch, and Git hits the same conflict every time. You resolve it, decide the rebase went wrong, run git rebase --abort, try again, and the identical conflict in checkout.ts is back. Your careful 20-line resolution is gone.
The same thing happens in other everyday workflows:
- Stacked branches. You rebase
feat-aonto main and fix a conflict. Then you rebasefeat-b(built onfeat-a) and get the same conflict again, becausefeat-bcarries its own copies offeat-a's commits. - Trial merges. You test-merge a long-lived branch to see what breaks, abort, and redo the whole merge for real later.
- Repeated rebases of a long-running branch. You run
git pull --rebaseon a branch that also has merges from a teammate, and the same commit replays against the same upstream change.
Each repeat costs minutes. Worse, each repeat is another chance to resolve it differently, and that is how subtly wrong code ships.
Why the Obvious Fix Falls Short
"Just use -X ours / -X theirs." git rebase -X theirs main makes the conflicts vanish. It does this by picking one side for every conflicting hunk in every file. Most real resolutions combine both sides: you keep upstream's renamed function and your new argument. A strategy option silently throws one half away. You get no prompt and no conflict markers, so the first sign of trouble is a broken build or a quietly lost fix.
"Squash first so I only resolve once." Squashing cuts a 10-commit replay down to one conflict. But it still gives you one conflict per rebase attempt, it destroys reviewable history, and it doesn't help with stacked branches or abort-and-retry.
"Merge instead of rebase." This is a legitimate policy choice, but it doesn't fix trial merges you abort. It also doesn't help when you need to back out a merge and redo it.
The real problem is that Git, by default, has no memory of how you resolved a conflict. A resolution only survives if it ends up inside a commit, and aborting or rewriting throws it away.
How It Actually Works
rerere stands for reuse recorded resolution. Once it is enabled, Git hooks into every conflicted merge, rebase, cherry-pick, and revert:
- Record the preimage. When a conflict occurs, Git takes each conflict hunk and normalizes it. The two sides are sorted, so it doesn't matter which side is "ours" and which is "theirs". It also strips the branch labels from the conflict markers. It then hashes the result and saves it as
.git/rr-cache/<hash>/preimage. - Record the postimage. When you resolve and then commit, continue the rebase, or run
git rerere, Git saves your resolved text as thepostimage. - Replay. The next time any operation produces a conflict hunk with the same normalized hash, Git writes your postimage into the working tree. You'll see
Resolved 'file' using previous resolution.
flowchart TD
A[merge / rebase / cherry-pick hits conflict] --> B[Normalize each hunk: sort sides, strip labels]
B --> C[Hash hunk text]
C --> D{rr-cache/hash/postimage exists?}
D -- yes --> E[Write recorded resolution into file]
E --> F{rerere.autoUpdate?}
F -- true --> G[Stage file automatically]
F -- false --> H[Leave unstaged for review]
D -- no --> I[Save preimage, leave conflict markers]
I --> J[You resolve by hand]
J --> K[commit / rebase --continue / git rerere]
K --> L[Save postimage to rr-cache]
The key point is that the cache is keyed on the content of the conflict, not on branches or commits. That is why a resolution recorded while rebasing feat-a gets reused when the same hunk appears in feat-b, or in a cherry-pick next week.
Before and After
Without rerere, an aborted rebase loses everything:
git rebase main
# CONFLICT (content): Merge conflict in src/checkout.ts
# ...spend 10 minutes resolving...
git add src/checkout.ts
git rebase --continue
# CONFLICT in src/pricing.ts -- wrong approach, start over
git rebase --abort # resolution for checkout.ts is discarded
git rebase -i main # retry with reordered commits
# CONFLICT (content): Merge conflict in src/checkout.ts <- same one, again
With rerere, you configure it once and resolutions survive aborts and rewrites:
# CHANGED: one-time global setup; Git now records every resolution
git config --global rerere.enabled true
git rebase main
# CONFLICT in src/checkout.ts
# Recorded preimage for 'src/checkout.ts'
# ...resolve...
git add src/checkout.ts
git rebase --continue # Recorded resolution for 'src/checkout.ts'
git rebase --abort
git rebase -i main
# Resolved 'src/checkout.ts' using previous resolution. <- replayed
git diff # CHANGED: still review it, since it is unstaged by default
git add src/checkout.ts && git rebase --continue
If you abort before continuing, record your work first by running git rerere while the files are resolved.
When NOT to Use This
- Conflicts that aren't textual hunks. rerere does nothing for modify/delete, rename/rename, or binary-file conflicts. You still resolve those by hand.
- Conflicts that keep changing. If upstream keeps editing the same lines, every conflict hashes differently and nothing gets reused. Rebase more often, or split the file, so you are not fighting churn.
- Resolutions that depend on context outside the hunk. rerere reuses text blindly. If the correct resolution depends on code elsewhere that has since changed, the replayed text compiles but is wrong. For generated files such as lockfiles, regenerate them instead (for example, run
npm install). - Sharing resolutions across a team. The cache is local and never pushed. For shared, repeatable integration, commit the resolution into a merge commit or an integration branch rather than relying on everyone's rr-cache.
Gotchas
- A bad resolution gets replayed forever. If you recorded a mistake, run
git rerere forget path/to/filewhile the conflict is active, then resolve again. rerere.autoUpdate=truehides the replay. Auto-staged files sail throughrebase --continuewithout you looking. Keep it off on shared or critical branches, and always rungit diffafter a reused resolution.- Resolutions expire.
git gcprunes unresolved records after 15 days and resolved ones after 60 (gc.rerereUnresolved,gc.rerereResolved). For long-lived release branches, raise these limits. - Recording happens on continue or commit, not on
git add. If you abort right after staging, nothing is saved unless you rungit rererefirst. - New clone, empty cache. CI runners and fresh laptops start with no recordings. You can seed a cache from historical merge commits with
contrib/rerere-train.shfrom Git's source tree. - Stacked branches. Combine rerere with
git rebase --update-refs, which moves every branch in the stack in a single rebase. You'll hit fewer duplicate conflicts in the first place.
Key takeaway: Run `git config --global rerere.enabled true` once, and Git will record each conflict resolution and replay it automatically the next time the same conflict hunk appears.
Real-world challenge
Your team keeps a long-lived release branch. Every Friday someone test-merges it into main to check for conflicts, resolves about 12 conflicts in config/ and migrations/, runs the tests, then runs `git merge --abort` because the real merge has to wait for sign-off. On Monday the real merge happens and the same 12 conflicts show up again, so someone has to resolve them all from scratch. Last month one of those re-resolutions picked the wrong side of a migration and caused a production incident. How do you fix the workflow?
Diagnosis: The Friday resolutions are thrown away by --abort. Git only keeps a resolution if a commit is created, and the trial merge never commits. On Monday a different person re-resolves the same conflicts by hand, which is where the wrong-side mistake came from.
Fix: Enable rerere for everyone on the team. Then make the Friday trial merge record its resolutions before the abort.
git config --global rerere.enabled true
# Friday trial merge
git checkout main && git merge release
# ...resolve all 12 conflicts...
git rerere # records postimages into .git/rr-cache
git diff # review
git merge --abort # recordings survive the abort
# Monday
git merge release # "Resolved 'config/app.yml' using previous resolution."
git diff # verify, then commit
Caveats:
.git/rr-cachelives on one machine only. Either do the Friday and Monday merges on the same clone, or build the cache on the merging machine withcontrib/rerere-train.shfrom past merge commits.- Leave
rerere.autoUpdateoff here, so the reused resolutions stay unstaged and someone has to review the migration files. - Unrecorded resolutions are pruned after 15 days and recorded ones after 60 (
gc.rerereUnresolved/gc.rerereResolved). A weekly cycle fits within that, but a quarterly one does not.