Skip to content

Git Rerere: Stop Resolving the Same Merge Conflict Twice

You rebase a long-lived branch onto main. The first commit conflicts in config.yml; you resolve it. The second commit touches the same lines and conflicts again — in exactly the same way. So does the third. Or you resolve a merge, abandon it, and a week later merge again and face the identical hunk. Each time you re-read both sides, re-make the same decision, and re-type the same resolution.

Git can remember it for you. git rererereuse recorded resolution — records how you resolved a conflict and, when the same conflict appears again, writes that resolution into the file for you. It is one configuration setting, and it comes with two rules that matter more than the setting: a replayed resolution is a suggestion you must read, and it stops being right the moment the code around it changes.

This article builds a repository where the same conflict appears twice, records a resolution on the first pass, and watches what Git does on the second. Everything runs in a disposable directory.

When a merge, rebase, cherry-pick or revert leaves a conflicted file, rerere stores a normalised copy of the conflicted hunk — the preimage: both sides of the conflict with the branch labels stripped, so the record is about the content that clashed, not the names on the markers. When you resolve the file and the resolution is committed (or the file is staged, depending on configuration), rerere stores the resolved hunk — the postimage — alongside it, under .git/rr-cache/<hash>/.

The next time a conflict produces the same preimage, rerere applies the postimage. Same content on both sides in, same resolution out. Different content on either side, no match, and you get an ordinary conflict to resolve by hand.

Three consequences follow, and the rest of the article demonstrates each:

  1. It is keyed on the conflict, not the operation — a resolution recorded during a merge replays during a rebase.
  2. By default it replays into the working tree only and leaves the file unmerged, so you must inspect and git add it. rerere.autoupdate changes that.
  3. A recorded resolution can be wrong, or become wrong. You can forget it.

Everything below runs in a temporary directory with its own HOME, so no global configuration is read or written and no existing repository is touched. The fixture script in the site repository, scripts/articles/rerere-demo.sh, does exactly this; the commands are shown here so you can follow along by hand.

Terminal window
WORK=$(mktemp -d)
export HOME="$WORK" GIT_CONFIG_NOSYSTEM=1 GIT_EDITOR=true
cd "$WORK"
git init -q -b main demo && cd demo
git config user.email lab@example.com
git config user.name "Lab User"
printf 'timeout: 3000\nretries: 3\n' > config.yml
git add . && git commit -q -m "Add config"
git switch -qc feature
printf 'timeout: 500\nretries: 3\n' > config.yml
git commit -qam "Fail fast: lower timeout"
printf 'timeout: 500\nretries: 3\nbackoff: exponential\n' > config.yml
git commit -qam "Add backoff"
git switch -q main
printf 'timeout: 10000\nretries: 3\n' > config.yml
git commit -qam "Raise timeout for slow networks"
git log --oneline --all --graph

Captured output:

* b5957c2 Add backoff
* c1359f5 Fail fast: lower timeout
| * 952f545 Raise timeout for slow networks
|/
* d64a592 Add config

Two branches, both changed the timeout line, in opposite directions. Now enable rerere for this repository onlygit config without --global writes to .git/config:

Terminal window
git config rerere.enabled true
git config rerere.enabled
true

Merge main into feature:

Terminal window
git switch -q feature
git merge main

Captured output:

Auto-merging config.yml
CONFLICT (content): Merge conflict in config.yml
Recorded preimage for 'config.yml'
Automatic merge failed; fix conflicts and then commit the result.

The third line is rerere at work: it has already stored the preimage. git rerere status lists the paths it is tracking in the current operation:

Terminal window
git rerere status
config.yml

The file has the usual markers:

<<<<<<< HEAD
timeout: 500
=======
timeout: 10000
>>>>>>> main
retries: 3
backoff: exponential

Resolve it — deliberately with a third value, so the resolution is clearly a decision and not “take one side”:

Terminal window
printf 'timeout: 5000\nretries: 3\nbackoff: exponential\n' > config.yml
git add config.yml
git commit -m "Merge main into feature: settle on 5000"

Captured output:

Recorded resolution for 'config.yml'.
[feature d4ea953] Merge main into feature: settle on 5000

Recorded resolution is the moment the postimage was written. The cache now has both halves:

Terminal window
ls .git/rr-cache/*/
postimage
preimage

Now pretend the merge was the wrong approach — the team wants a rebase instead. Undo the merge commit and put the branch back exactly as it was:

Terminal window
git reset -q --hard ORIG_HEAD
git log --oneline -1
b5957c2 Add backoff
Predict before you run it: what happens on git rebase main?

The first commit on feature changes timeout from 3000 to 500; main changed it from 3000 to 10000. That is the same clash as before — same two sides. Make a call on each of these before reading on:

  1. Does Git report a conflict at all?
  2. What is in config.yml when the rebase stops — markers, or a resolved line?
  3. Is the file staged?
  4. Does the rebase continue on its own?
Terminal window
git rebase main

Captured output:

Rebasing (1/2)Auto-merging config.yml
CONFLICT (content): Merge conflict in config.yml
error: could not apply c1359f5... Fail fast: lower timeout
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Resolved 'config.yml' using previous resolution.
Could not apply c1359f5... Fail fast: lower timeout

The answers: (1) yes, Git still reports CONFLICT and stops — rerere does not hide the conflict; (2) the file already contains the resolution; (3) it is not staged; (4) the rebase does not continue. Look at the file and the index:

Terminal window
cat config.yml
git status --short
timeout: 5000
retries: 3
UU config.yml

UU — unmerged — even though there are no markers. That is the default and it is the right default: the resolution was written for you, but you have not said it is correct.

Inspecting a reused resolution before accepting it

Section titled “Inspecting a reused resolution before accepting it”

Two views. git diff during a conflict shows the combined diff — what the resolved file looks like relative to each parent:

Terminal window
git diff
diff --cc config.yml
index 934c433,4a5691a..0000000
--- a/config.yml
+++ b/config.yml
@@@ -1,2 -1,2 +1,2 @@@
- timeout: 10000
-timeout: 500
++timeout: 5000
retries: 3

Read the two leading columns: - means the line came from the first parent only, - from the second only, ++ is in neither — it is the resolution. That single ++ line is exactly what you must check: is 5000 still the right answer for this rebase? Here it is. Accept it:

Terminal window
git add config.yml
git rebase --continue
git log --oneline
cat config.yml
[detached HEAD 78cc3a6] Fail fast: lower timeout
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/feature.
8fc3328 Add backoff
78cc3a6 Fail fast: lower timeout
952f545 Raise timeout for slow networks
d64a592 Add config
timeout: 5000
retries: 3
backoff: exponential

The second commit, “Add backoff”, applied without conflict — it only adds a line. A rebase of a ten-commit branch where three commits touch the same hunk is where rerere earns its keep: the same replay, three times, each one a git diff and a git add instead of a fresh decision.

git rerere diff is the other view: the difference between the recorded preimage and what is in the working tree now. It is most useful while you are still editing, to see how your resolution differs from the raw conflict.

Why is the file left unstaged? Because staging is your signature on the resolution. rerere is confident the conflict matches; it cannot know whether the decision still applies.

If you trust your recorded resolutions — typically on a branch you rebase repeatedly against the same base — rerere.autoupdate stages the replayed file as well:

Terminal window
git reset -q --hard ORIG_HEAD
git config rerere.autoupdate true
git rebase main

Captured output (the conflict lines are identical to before; the difference is one word):

CONFLICT (content): Merge conflict in config.yml
Staged 'config.yml' using previous resolution.
Terminal window
git status --short
M config.yml

M in the first column: staged. The rebase still stops — rerere never continues an operation on its own — but git rebase --continue now proceeds without a manual git add. On a branch with a dozen identical conflicts that is a dozen stops and a dozen --continues; each stop is a chance to notice that something looks different, which is why the stop is not optional.

Turn autoupdate off again for the rest of the walkthrough: git config --unset rerere.autoupdate.

Suppose 5000 was wrong — the team has since decided main’s 10000 must win. The recorded resolution will keep reinserting 5000. Discard it, during the conflict, with git rerere forget:

Terminal window
git reset -q --hard ORIG_HEAD
git rebase main
git rerere forget config.yml

Captured output (rebase lines omitted):

Resolved 'config.yml' using previous resolution.
Updated preimage for 'config.yml'
Forgot resolution for 'config.yml'

forget removes the postimage. It does not put the conflict markers back in the working tree — the file still contains the replayed 5000. To resolve afresh, ask Git to re-create the conflicted version of the file from the index:

Terminal window
git checkout --conflict=merge config.yml
cat config.yml
Recreated 1 merge conflict
<<<<<<< ours
timeout: 10000
=======
timeout: 500
>>>>>>> theirs
retries: 3

Now resolve it the new way, git add, and continue; the new resolution is recorded in place of the old one. (This walkthrough aborts the rebase instead: git rebase --abort.)

Two related commands, for completeness: git rerere clear discards the metadata for the in-progress operation (what rerere would record if you committed now) without touching stored resolutions, and git rerere gc prunes old entries — unresolved preimages after 15 days and resolved ones after 60 by default (gc.rerereUnresolved, gc.rerereResolved).

Limitations, and when a human must look again

Section titled “Limitations, and when a human must look again”

A different conflict is a different conflict. Change either side and nothing is replayed. Add a commit on main that also changes retries, then merge again:

Terminal window
git switch -q main
printf 'timeout: 10000\nretries: 5\n' > config.yml
git commit -qam "More retries"
git switch -q feature
git merge main
Auto-merging config.yml
CONFLICT (content): Merge conflict in config.yml
Recorded preimage for 'config.yml'
Automatic merge failed; fix conflicts and then commit the result.

No “using previous resolution”: the hunk now spans two lines on main’s side, so the preimage is new. That is the safety property — rerere only speaks when the conflict is byte-for-byte the one it saw before.

The same conflict can still deserve a different answer. The preimage matches on the conflicted lines, not on the surrounding file, the tests, or the reason the code exists. If a function’s contract changed between the first resolution and the replay, the replayed lines can be syntactically perfect and semantically wrong. This is why the default leaves the file unstaged, and why “same conflict” is not the same as “same correct resolution”. Read the ++ lines every time.

It is local. .git/rr-cache is not pushed, cloned or shared. A colleague rebasing the same branch resolves the same conflicts by hand. Some teams script git rerere training on a shared history (contrib/rerere-train.sh in Git’s source) to seed the cache; that is a deliberate, reviewed step, not a default.

It records what you committed, including mistakes. A resolution that leaves a stray marker or a wrong value is recorded just as faithfully. Catch it with the same habit that catches ordinary conflict mistakes: a marker grep and the test suite before every --continue.

Terminal window
grep -nE '^(<<<<<<<|=======|>>>>>>>)' config.yml && echo "MARKERS REMAIN" || echo "clean"
  • Expecting rerere to finish the rebase. It resolves the file; you continue the operation. With autoupdate it also stages; it still never continues.
  • Enabling it after the first conflict and expecting the second to replay. The preimage is recorded when the conflict occurs. Enable it before the operation that first conflicts.
  • Treating “Resolved using previous resolution” as “verified”. It means “matched”. Inspect with git diff before git add.
  • Forgetting with the wrong tool. git rerere clear does not remove a recorded resolution; git rerere forget <path> does, and git checkout --conflict=merge <path> brings the markers back to resolve again.
  • Assuming it is shared. It is per-clone.

rerere is only useful once resolving a conflict by hand is routine — the replayed content is a resolution you must be able to judge in seconds. Two labs build that, in a disposable repository with real output:

The lesson behind both, including the mental model for reading markers, is Resolving merge conflicts in Git; the rebase side is When not to rebase, which explains why repeated rebases of a shared branch — the very situation that makes rerere attractive — are usually the thing to avoid.

How this article was verified

Every command was run on 17 September 2026 in a freshly created temporary repository with an isolatedHOME, a throwaway identity and hooks disabled — never against a real repository. Output blocks labelled captured are pasted from that run; blocks labelled illustrativeare described rather than pasted. Versions: git 2.43.0 on Ubuntu 24.04. The fixture script is scripts/articles/rerere-demo.sh in the site repository; it rebuilds the example from scratch. Primary reference: git-scm.com/docs/git-rerere.

How did this go?