Merge Strategies, Drivers, and rerere
Control exactly how Git resolves a merge: ort and the other strategies, -s ours versus -X ours, custom merge drivers in .gitattributes, and rerere for recurring conflicts.
Control exactly how Git resolves a merge: ort and the other strategies, -s ours versus -X ours, custom merge drivers in .gitattributes, and rerere for recurring conflicts.
You have resolved this conflict before. Same file, same two hunks, same rebase of the same long-lived branch — and Git asks again as if you had never met. That repetition is not the cost of doing business; it is a sign you are doing by hand what Git will do for you.
Merging is not one algorithm with one answer. It is a pluggable system with a strategy, a set of options, per-file drivers, and a memory. By the end you can steer all four: pick the algorithm, bias the tie-breaks, hand a file type its own merge rule, and teach Git a resolution once so it replays forever.
A merge strategy is the program Git runs to combine two or
more histories. You select one with -s.
ort is the default since Git 2.34 and the one you almost always
want: a full three-way merge with rename detection and recursive
handling of multiple merge bases. resolve is the old, simple
three-way merge, which picks a single merge base and skips the
recursive dance when history has criss-crossed — occasionally
useful precisely because it is dumber.
octopus merges more than two branches in one commit and is
chosen automatically when you name several; it refuses to run if
any side needs manual resolution, so it is a tidy-up tool for
independent topic branches, not a conflict solver. subtree is
ort with an adjustment: it shifts one tree so a project living
in a subdirectory lines up with one living at the root, which is
what makes subtree-style vendoring merge sensibly.
Then there is ours, which is not really a merge at all.
git merge -s ours legacy-2x # keep our tree, record the parentThe result keeps your tree byte for byte and records the other
branch as a second parent. Its legitimate use is bookkeeping. Once
a release branch has been folded into main some other way —
cherry-picked, reworked, abandoned — -s ours marks it merged so
future merges and git branch --merged skip it.
ort — "Ostensibly Recursive's Twin" — replaced recursive
because the old strategy was slow and wrong in the same place:
renames. Where recursive built results by writing index entries
and touching the working tree as it went, ort computes the whole
merge in memory first — dramatically faster on large trees, and
able to reason about every rename at once.
The clearest win is directory rename detection. Your teammate
renames src/ to lib/ on their branch; meanwhile you add a new
file src/parser.go on yours. A naive merge keeps both facts
literally, leaving you lib/ full of moved files plus a lonely
src/parser.go that no build system knows about. ort sees that
the directory moved and places your new file at lib/parser.go:
-s picks the algorithm; -X passes a strategy option to
that algorithm. -s ours and -X ours share three letters and
nothing else, and this is the distinction people get wrong most
often.
Both branches touch config.yml: feature adds a timeout key
at the bottom, and both sides edited the retries line
differently.
You get the new timeout key, because that hunk did not conflict.
You get your retries line, because that hunk did. Compare with
git merge -s ours feature, which hands back your file exactly as
it was — no timeout key at all.
The other -X options are quieter but earn their keep:
-X renormalize deserves a word. If a file was committed with
CRLF endings before someone added * text=auto to
.gitattributes, the two sides can differ on every line purely by
invisible characters. renormalize runs a virtual check-out and
check-in of all three versions through your current
.gitattributes rules first, so the merge compares real content
instead of phantom changes.
Sometimes the right behaviour depends on the file and no global
option helps. A merge driver is a program you register once
and attach to paths through .gitattributes — a house rule for
one kind of file. Git ships three: text (the normal three-way
merge), binary (refuse to merge textually), and union. Start
with union, which suits append-only files:
union keeps both sides' lines, in order, with no conflict
markers — exactly right when two branches each appended an entry.
A custom driver is a command line plus placeholders. Say a generated file should always keep the local version:
true is the shell command that does nothing and exits 0. A
driver is handed our version in a temporary file and must leave
the result there, so doing nothing means "ours wins".
The full form uses placeholders:
%O is the common ancestor, %A ours, %B theirs — each a
temporary file. %L is the conflict marker length and %P the
real pathname in the working tree. The contract: write the result
into the file named by %A, then exit 0 for a clean merge or
non-zero to report a conflict. Git keeps whatever you left in %A
either way, so a good driver writes conflict markers before
failing.
The binary attribute is the blunt version of the same idea: Git
never attempts a textual merge, and on divergence it leaves your
version in the working tree and records a conflict, so you resolve
by choosing a whole side rather than editing hunks.
rerere stands for "reuse recorded resolution", and it is the
most valuable under-used feature in Git. When it is on and you hit
a conflict, Git records two things in .git/rr-cache: a
normalised signature of the conflict, and — once you finish — the
resolution you chose. Meet that signature again, in any branch,
any merge, any rebase, and Git applies the same resolution
automatically. Think of it as an apprentice watching over your
shoulder: explain the case once, and they handle it from then on.
Three workflows where it changes your week.
The second rebase is silent.
Every rebase replays your commits against a moving target, so the same conflict returns each time.
What you solved stays solved.
You merge, find the integration wrong, git reset --hard
back, fix something upstream, and merge again.
No human in the loop at all.
CI merges six topic branches into a throwaway branch nightly, and rerere keeps the recurring conflicts resolved.
The inspection commands matter as much as the automation:
Reach for git rerere diff after an automatic replay: it shows
the conflicted starting point against the current content, so you
read what rerere did on your behalf instead of trusting it blind.
You do not have to create a merge to see what it does. Stop one step short:
--no-ff matters: without it a fast-forwardable branch only moves
the pointer and there is nothing to inspect.
For tooling and CI there is something better, because it never touches your working tree or index:
This merges in memory, writes the resulting tree object, and
prints its ID. The exit status is non-zero when the merge
conflicts, which makes it a one-line mergeability check for a bot
running against a bare repository. Add --name-only to list
conflicted paths instead.
For merges that already exist, there is a review tool almost nobody knows about:
Git redoes the merge cleanly and diffs the real commit against that reconstruction, so what you see is exactly what the human changed while resolving — not the whole merge, only their edits. It is the fastest way to catch a resolution that dropped someone's bug fix.
You can now aim a merge instead of enduring it: a strategy for the
shape of the history, -X for the tie-breaks, a driver for the
file type, and rerere for the conflict you have already solved.
Next comes working on two branches at once — checking out
several branches at once without stashing, and the two very
different ways Git nests one repository inside another. The
subtree strategy you met here is the machinery behind one of
them, so you are already half introduced.
Before you move on, spend ten minutes on something real. Turn rerere on, find a branch in your own repo that conflicts on rebase, resolve it, and rebase it again. Watching Git replay your own answer is the moment this stops being theory.
CONFLICT (file location): src/parser.go added in HEAD inside a
directory that was renamed in origin/refactor, suggesting it
should perhaps be moved to lib/parser.go.git merge -X ours featuregit merge -X ignore-space-change feature # reindent-proof
git merge -X patience feature # cleaner hunk pairing
git merge -X diff-algorithm=histogram feat # faster, similar gain
git merge -X find-renames=40% feature # loosen rename detect
git merge -X renormalize feature # fix line-ending churngit config merge.ours.driver truegit rerere status # paths whose conflict was recorded
git rerere diff # what changed since the conflict state
git rerere remaining # paths still needing your hand
git rerere forget config/app.yml # drop a wrong resolutiongit merge --no-commit --no-ff feature
git diff --cached HEAD # inspect the merged result
git merge --abort # or commit itgit merge-tree --write-tree main featuregit show --remerge-diff <merge-commit>git merge -s ort feature # default: renames, dir renames
git merge -s resolve feature # simple non-recursive 3-way
git merge -s octopus a b c # many branches, no conflicts
git merge -s subtree feature # shift trees to line up
git merge -s ours legacy-2x # mark superseded; DISCARDS theirs
git merge -X ours feature # real merge; ours wins conflicts
git merge -X theirs feature # real merge; theirs wins them
git merge -X ignore-space-change f # ignore whitespace churn
git merge -X patience feature # patience diff for hunk pairing
git merge -X diff-algorithm=histogram f
git merge -X find-renames=40% f # loosen rename similarity
git merge -X renormalize feature # fix CRLF/line-ending churn
git config merge.ours.driver true # define the "ours" driver
git config --global rerere.enabled true
git config --global rerere.autoUpdate true
git rerere status # recorded conflicts in progress
git rerere diff # what the resolution changed
git rerere remaining # paths still unresolved
git rerere forget <path> # unlearn a bad resolution
git merge --no-commit --no-ff f # stage the merge, do not commit
git diff --cached HEAD # inspect that staged merge
git merge --abort # back out of an in-progress merge
git merge-tree --write-tree a b # merge in memory; CI-friendly
git show --remerge-diff <commit> # what the resolver actually did# .gitattributes
CHANGELOG.md merge=union# .gitattributes
config/generated-schema.json merge=ours[merge "json-sorted"]
name = order-insensitive JSON merge
driver = merge-json %O %A %B %L %P