Merge Strategies & gitattributes Jump to heading

Most conflict pain is treated as an unavoidable cost of collaboration, and a surprising amount of it is configuration. Git’s merge machinery is pluggable at three levels β€” the strategy that decides the overall approach, the options that tune it, and per-path drivers that replace it entirely for specific files β€” and almost nobody touches any of them. The result is teams hand-resolving conflicts in files that have a deterministic correct resolution, and arguing about whitespace changes Git can be told to ignore. This part of Conflict Resolution & Safe Merge Operations covers all three levels.

Prerequisites Jump to heading

The Three Levels of Control Jump to heading

Knowing which level a problem belongs to is most of the work. A conflict caused by a whitespace change is a strategy-option problem; a conflict in a lockfile is a driver problem; a conflict in genuinely overlapping logic is neither, and no configuration will help.

Three levels, three kinds of problemThe strategy decides the overall algorithm and is rarely worth changing. Strategy options tune how it treats whitespace and how it resolves ties, and they are legitimate for specific situations. Per-path drivers replace merging entirely for files where a textual merge has no correct answer.Strategyort by defaultrarely changedwhole-merge scopeStrategy options-X ignore-all-space-X ours / theirsper mergePer-path drivermerge=<name>attributes decideregenerate insteadmost teams only ever use the first column, and most of their pain is in the other two

Step 1 β€” Know What the Default Strategy Does Jump to heading

Since Git 2.33 the default is ort, a reimplementation of the old recursive strategy. It produces better results on renames and is substantially faster on large repositories, and it occasionally resolves something recursive would have conflicted on.

# Which strategy is in use?
git -c merge.verbosity=5 merge --no-commit --no-ff other-branch 2>&1 | head -5
git merge --abort
# Compare the two explicitly on a merge you already know conflicts
git merge -s ort      --no-commit other-branch 2>&1 | tail -3; git merge --abort
git merge -s recursive --no-commit other-branch 2>&1 | tail -3; git merge --abort
# Verification: your client's default
git --version
git config --get pull.twohead || echo "(unset β€” ort on 2.33+)"

The practical advice is to leave the strategy alone. The comparison in choosing between the ort and recursive strategies covers the rare cases where the older one behaves differently enough to matter.

Step 2 β€” Use Strategy Options Where They Are Honest Jump to heading

-X options change how a merge resolves, and two of them are genuinely useful while one is frequently misused.

# Legitimate: a reformatting commit on one side created whitespace-only conflicts
git merge -X ignore-all-space other-branch

# Legitimate: a generated file where one side is authoritative for this merge
git merge -X ours other-branch          # prefer our side ONLY in conflicting hunks
# Verification: what did the option actually change?
git diff HEAD@{1} HEAD --stat
git log -1 --format='%H %s'

SAFETY WARNING β€” -X ours resolves conflicting hunks in favour of your side without telling you what it discarded, and it is not the same as -s ours, which discards the other branch’s changes entirely. Both silently drop work, and a merge commit produced by either looks exactly like a normal one in history. Use them only where the discarded content is genuinely regenerable, and record in the commit message that you did.

Step 3 β€” Let Attributes Decide Before a Conflict Exists Jump to heading

The most effective control is per path, declared once, applying to everyone automatically.

cat >> .gitattributes <<'ATTR'
# Generated: regenerate rather than merge
package-lock.json   merge=lockfile   linguist-generated=true
schema.graphql      merge=generated

# Append-only: keep both sides
CHANGELOG.md        merge=union
docs/decisions/*.md merge=union

# Binary: never attempt a textual merge
*.png               binary
*.xlsx              binary -merge
ATTR
# Verification: what will Git do with each path on a conflict?
git check-attr merge -- package-lock.json CHANGELOG.md assets/logo.png

merge=union keeps both sides’ lines for append-only files, which turns a guaranteed conflict in a changelog into no conflict at all β€” the detail is in union merge for append-only files.

Step 4 β€” Define Drivers for Files With a Deterministic Resolution Jump to heading

A driver is a command Git runs instead of merging. For generated files the correct resolution is always β€œregenerate”, and a driver expresses that.

git config merge.lockfile.name 'regenerate the lockfile from the manifest'
git config merge.lockfile.driver 'cp %B %A && npm install --package-lock-only --silent'

git config merge.generated.name 'regenerate from source'
git config merge.generated.driver 'npm run codegen --silent && cp %A %A'
# Verification: force a conflict and confirm it resolves without intervention
git merge other-branch 2>&1 | grep -i 'package-lock' || echo "lockfile resolved automatically"

Drivers live in local configuration rather than in the repository, because a driver is a command and honouring commands from a cloned repository would be a security problem. Distribute them through the include mechanism in shipping a team gitconfig with includeIf.

Where each level intervenes during a mergeAttributes are consulted per path before any merging happens, and can route a file to a driver or mark it binary. The strategy handles everything left over, with options tuning how it treats whitespace and ties. Only what survives all three reaches the reader as a conflict.Attributesper pathbinary, union, driverDriverregeneratedeterministicStrategyort, three-wayConflictgenuinely overlappingthe goal is that everything reaching the last box is a real decision Three files, three correct answersA source file with overlapping edits genuinely needs a person. A changelog where both sides appended needs both sets of lines. A lockfile needs regenerating from the merged manifests, because a textual merge of a resolved dependency graph is meaningless.What conflictsCorrect resolutionsource editsoverlapping hunksa person decideschangelogboth sides appendedkeep both β€” unionlockfilerewritten on both sidesregenerate β€” driverbinary documentno textual mergekeep both filesonly the first row is a merge conflict in the useful sense of the term

Step 5 β€” Make Binary Files Fail Fast Jump to heading

A binary file with no merge tool produces a conflict nobody can resolve textually, and the worst outcome is somebody resolving it anyway.

# Mark it binary so Git refuses rather than producing a corrupt file
printf '*.xlsx binary\n*.psd binary\n' >> .gitattributes
# And define what to do instead, per format
git config merge.keepboth.name 'keep both versions side by side'
git config merge.keepboth.driver 'cp %A "%A.ours" && cp %B "%A.theirs" && exit 1'
# Verification: the conflict names both files for a human to reconcile
git check-attr merge -- report.xlsx

Failing loudly with both versions on disk is better than any automatic resolution, because a person with the two files and the right application can produce the correct answer, and Git cannot. The approach is covered further in resolving complex binary conflicts in git.

Choosing the Right Level for a Recurring Conflict Jump to heading

When the same file conflicts repeatedly, work down the levels rather than up. The first question is whether the file is source at all β€” if a command reproduces it, the answer is a driver and the conflict disappears permanently. The second is whether the file is append-only, in which case union merge removes the conflict without any command. The third is whether the conflicts are caused by formatting rather than content, which is a strategy-option question and usually also a signal that a formatter should run in a hook rather than in occasional sweeps.

Only after all three should a recurring conflict be treated as a genuine design problem β€” two areas of responsibility colliding in one file β€” which is the subject of conflict prevention by design. Getting the order wrong is common and expensive: teams restructure a module to avoid conflicts that a three-line attribute would have removed, or configure an attribute to paper over a genuine coordination problem that then surfaces somewhere less visible.

It is also worth being precise about what these mechanisms can and cannot promise. Attributes and drivers make merges deterministic; they do not make them correct. A lockfile regenerated from two merged manifests is internally consistent and may resolve a dependency to a version neither branch tested. A union-merged changelog contains both sides’ entries in an order nobody chose. In both cases the automation is doing the right thing and the result still needs the pipeline to run against the merged tree β€” which is the argument for a merge queue, covered in merge queues and required checks.

Finally, document the attributes as you would any other rule. A merge=union on a file that later stops being append-only produces duplicated content nobody can explain, and a driver that regenerates a file whose generator has been removed fails in a way that looks like Git being broken. A comment per line, and an occasional review of what the file still claims, keeps the mechanism trustworthy.

Configuration Reference Jump to heading

Attribute or settingEffectWhen to use
merge=unionKeeps both sides’ linesAppend-only files: changelogs, decision logs
merge=<driver>Runs a command instead of mergingGenerated files with a deterministic resolution
binaryNo textual merge or diffImages, documents, archives
-mergeConflict always, never auto-resolveFiles where a wrong merge is worse than a conflict
-X ignore-all-spaceIgnores whitespace differencesAfter a reformatting commit on one side
-X ours / -X theirsPicks a side in conflicting hunks onlyRegenerable content, recorded in the message
-s oursDiscards the other branch’s changes entirelyAlmost never β€” it records a merge that merged nothing
merge.conflictStyle=zdiff3Shows the common ancestorAlways; it turns guessing into deciding

Troubleshooting Jump to heading

SymptomLikely causeFix
Lockfile conflicts on every mergeTreated as sourceAdd a merge driver that regenerates it
Changelog conflicts on every releaseTwo branches appended at the same placemerge=union
Whole file conflicts after a reformatWhitespace changes on one side-X ignore-all-space for that merge only
A driver does nothingDriver defined in the repository, not locallyDistribute it via the config include
Binary file merged into corruptionNot marked binaryMark it, and define a keep-both driver
Merge silently dropped a change-X ours or -s ours was usedCheck the merge commit; prefer explicit resolution

Frequently Asked Questions Jump to heading

Should we change the default merge strategy? Jump to heading

No. ort is better than its predecessor on essentially every axis, and the cases where the old behaviour differed are rare enough that discovering one is more likely to indicate a genuine conflict than a strategy problem. Spend the effort on attributes, which apply automatically and to everyone.

Is merge=union safe? Jump to heading

For genuinely append-only files, yes β€” it produces both sides’ lines with no conflict. For anything else it is dangerous, because it will happily interleave two versions of a function into something that neither compiles nor resembles either side. Apply it per path, never as a default.

Why can drivers not be committed to the repository? Jump to heading

Because a driver is a shell command, and honouring commands from a cloned repository would mean cloning could execute code. Git therefore reads driver definitions only from local configuration. The attributes that reference a driver are committed; the definition has to be installed, which is what the configuration include is for.

Do these settings apply to rebases and cherry-picks as well as merges? Jump to heading

Yes β€” all of them use the same merge machinery, so attributes, drivers and the conflict style apply identically. Strategy options can be passed to rebase and cherry-pick with -X as well, which is useful when replaying a branch across a reformatting commit.

Can attributes make a merge wrong? Jump to heading

They can make it wrong quietly, which is the risk worth naming. A union merge on a file that stopped being append-only, a driver whose generator has been removed, a binary mark on a file people expect to diff β€” each produces a result that looks fine and is not, and none of them raises a conflict to alert anyone. The mitigation is a periodic review of the attributes file alongside a check in the pipeline that the files it governs still behave the way the attribute assumes: generated output still regenerates identically, append-only files still have no in-place edits, binary files still have no textual representation anyone relies on.