Skip to content

Advanced Git Merge Strategies: ort, Options and Merge Drivers

Lesson 7 of 7Intermediate → Advanced11 min readModern Git Workflows · MergingVerified: Git 2.43.0 on Ubuntu 24.04; strategy behaviour checked against the current git-merge-strategies documentation

Git can merge in more than one way. The strategy is the algorithm; strategy options tune that algorithm’s behaviour. Confusing the two is not a pedantic distinction — -s ours and -X ours sound alike and one of them silently discards an entire branch.

Almost all everyday merging uses the default with no options. This lesson covers when that is not enough, and marks clearly which techniques are niche.

For a two-branch merge, current Git uses the ort strategy. It performs the three-way merge described in Lesson 1: find the merge base, compute what each side changed, combine.

ort also handles the cases that make real merges awkward:

  • Multiple merge bases. When branches have diverged and re-converged, there can be more than one candidate common ancestor. ort merges the ancestors themselves to produce a synthetic base, then merges against that.
  • Renames. It detects files that moved and applies the other side’s changes to the new location.
  • Directory renames. If one side moved a directory and the other added a file to the old location, it can place the new file in the moved directory.
  • Submodules. It fast-forwards submodule pointers where that is unambiguous.

You will not normally name it. git merge feature uses it.

Older documentation, and a great deal of older writing, refers to recursive as the default. That was correct for many years.

ort replaced it as the default in Git 2.34. In current Git, recursive is a synonym for ort — selecting it does not get you the old implementation, which has been removed.

This is the distinction that matters most.

FlagWhat it does
Strategy-s <name>Selects the merge algorithm
Strategy option-X <name>Passes an option to the algorithm

-s ours and -X ours are entirely different operations, and the difference is destructive.

Terminal window
git merge -X ours feature

Merges normally. Where a hunk conflicts, take our version rather than stopping. Non-conflicting changes from the other branch are still applied.

-s ours — discard the other branch entirely

Section titled “-s ours — discard the other branch entirely”
Terminal window
git merge -s ours feature

Produces a merge commit whose tree is exactly our current tree. Every change from the other branch is discarded — not just conflicting ones.

A branch that changes one line (conflicting with ours) and adds a new file (not conflicting at all):

Terminal window
git merge -X ours side
Auto-merging f.txt
Terminal window
grep '^b' f.txt # our version of the conflicting line
ls side.txt # the new file IS present
b-main
side.txt

Now the same merge with the strategy instead:

Terminal window
git merge -s ours side
Terminal window
grep '^b' f.txt
ls side.txt
b-main
ls: cannot access 'side.txt': No such file or directory

The file is gone. -s ours kept our tree wholesale, so the branch’s non-conflicting addition vanished — while the history records the branch as merged.

Recording that a branch is superseded. An abandoned branch whose work was redone differently. Merging with -s ours marks it integrated so it stops appearing as unmerged, without changing any files.

Blocking a forward-port. In a maintenance workflow, recording that a release branch’s changes should not propagate to main because main already solves the problem differently.

In both cases the intent is bookkeeping, and it is worth saying so in the commit message.

StrategyHeadsPurpose
ort2The default
recursive2Synonym for ort in current Git
resolve2Simpler 3-way merge; no rename detection; picks one merge base
octopus3+Default when merging more than two branches; refuses anything needing manual resolution
oursAnyKeep our tree entirely
subtree2ort adjusted so one tree is merged as a subtree of the other

Merging several branches at once:

Terminal window
git merge feat-a feat-b feat-c -m "Integrate topic branches"
Fast-forwarding to: feat-a
Trying simple merge with feat-b
Trying simple merge with feat-c

The resulting commit has one parent per branch. The critical limitation: octopus refuses to run if any side conflicts. It is for bundling branches you already know merge cleanly, typically into a throwaway integration branch for testing. If it refuses, merge them one at a time.

When one repository’s content has been embedded under a subdirectory of another, the two trees have different shapes and a normal merge sees every file as added or deleted. The subtree strategy shifts one tree to align:

Terminal window
git merge -s subtree --squash vendor/upstream

This is a specialised workflow. Most teams vendoring dependencies today use submodules, a package manager, or a dedicated tool rather than subtree merges.

An older, simpler algorithm: one merge base, no rename detection. It is very rarely the right choice. Its one occasional use is when ort’s handling of a criss-cross history produces a result you do not want and you deliberately want the simpler treatment.

All of these are -X options passed to ort.

Terminal window
git merge -X ours feature # our side wins conflicting hunks
git merge -X theirs feature # their side wins conflicting hunks

Safer than -s ours and safer than git checkout --ours <file>, because non-conflicting changes from both sides still land. Still a blunt instrument: review the result.

-X theirs is genuinely useful when re-applying a branch that you know supersedes the current state — regenerated files, a vendored update.

Terminal window
git merge -X ignore-space-change feature
git merge -X ignore-all-space feature
git merge -X ignore-space-at-eol feature
git merge -X ignore-cr-at-eol feature

These prevent conflicts caused purely by reindentation or line-ending differences. ignore-cr-at-eol is particularly useful on mixed Windows/Unix teams.

Terminal window
git merge -X find-renames=50 feature # similarity threshold, percent
git merge -X no-renames feature # disable detection entirely

Git does not record renames. It infers them by comparing content similarity between deleted and added files. The default threshold usually works; lowering it helps when a file was renamed and substantially edited.

On very large merges, Git may skip rename detection for performance and tell you so. Raising the limit restores it:

Terminal window
git config merge.renameLimit 10000
Terminal window
git merge -X diff-algorithm=patience feature
git merge -X diff-algorithm=histogram feature
git merge -X diff-algorithm=minimal feature
git merge -X diff-algorithm=myers feature

The algorithm determines how Git decides which lines correspond. patience avoids matching on low-information lines — a lone } or a blank line — which can otherwise produce a technically minimal but nonsensical diff, and therefore a conflict in the wrong place.

ort already defaults to histogram, which is patience-like with better performance. patience and histogram also exist as bare -X options, but those spellings are deprecated in favour of diff-algorithm=.

Terminal window
git merge -X renormalize feature

Runs a virtual check-out and check-in of all three merge stages before comparing. This is the fix for merges that conflict on every line because two branches were committed under different line-ending or clean-filter rules. If a merge shows an entire file as changed and the visible text is identical, try this.

For file types where line-based merging is meaningless, you can tell Git how to handle them per path.

*.png binary
*.pdf binary
*.xlsx binary

Git will not attempt a textual merge; a conflict becomes “choose one version”, which is the honest outcome.

package-lock.json merge=ours
CHANGELOG.md merge=union

merge=union is built in and concatenates both sides’ additions instead of conflicting — appropriate for append-only files such as changelogs, and inappropriate for anything where order or uniqueness matters.

merge=ours requires defining the driver:

Terminal window
git config merge.ours.driver true

The driver is a command Git runs; true succeeds without modifying the file, which leaves our version in place.

A merge driver is any command Git invokes with the three versions:

Terminal window
git config merge.jsonsort.name "Sorted JSON merge"
git config merge.jsonsort.driver "my-json-merge %O %A %B %L %P"

Placeholders: %O is the base file, %A is ours, %B is theirs, %L is the conflict marker size, and %P is the final path. The driver writes the result into %A and exits zero for success or non-zero to signal a conflict.

A decision table for the cases where the default is not obviously right.

SituationReach forNot
Ordinary mergeNothing — the default
Conflicts are all reindentation-X ignore-space-changeResolving fifty hunks by hand
Whole file conflicts, text looks identical-X renormalize-X ignore-all-space
A file was renamed and heavily edited-X find-renames=40-X no-renames
Regenerated file; incoming version is authoritative-X theirs, or merge=ours in .gitattributesManual resolution each time
Branch superseded; record it as integrated-s ours, with an explanatory message-X ours
Bundling several clean branches for a test build-s octopus (the default for 3+)Sequential merges
Binary assets conflictingMark binary in .gitattributesAny textual strategy
Changelog conflicts on every mergemerge=union in .gitattributes-X theirs

Two habits keep this safe. First, prefer a durable fix over a per-merge flag: .gitattributes and a formatter address the cause, whereas -X ignore-space-change addresses one merge. Second, whenever you pass a non-default strategy or option, say so in the merge commit message — the resulting tree gives no clue that anything unusual happened, and a future reader will otherwise assume a normal merge.

Strategy options make it more important than usual to check the outcome.

Terminal window
git show --cc HEAD

The combined diff shows only regions where the result differs from both parents — hand resolutions and anything a strategy option decided for you. On a merge performed with -X theirs, this is where you see which of your changes were dropped.

Terminal window
git diff HEAD^1 HEAD --stat
git diff HEAD^2 HEAD --stat

Comparing the merge result against each parent separately answers “what did this merge change, from each side’s point of view?” A -s ours merge shows an empty diff against the first parent and a large one against the second — which is the signature of a merge that recorded integration without integrating.

When two branches have merged from each other and then diverged again, there can be several equally valid merge bases. ort handles this by merging the candidate bases into a synthetic ancestor and merging against that — which is where the name’s “recursive” ancestry comes from.

You usually will not notice. When you do, it is because a conflict appears in code neither branch seems to have touched recently, and the explanation is that the synthetic base differs from what you expected.

Inspect the candidates:

Terminal window
git merge-base --all main feature

More than one line means a criss-cross history.

No strategy protects against semantic conflicts — changes that combine cleanly and behave incorrectly. Strategy options make this more likely, because -X ours, -X theirs and whitespace-ignoring options all suppress signals Git would otherwise raise.

If you use them, run the tests. If you use -s ours, say so in the commit message.

Confusing -s ours with -X ours. The strategy discards the branch entirely; the option resolves conflicting hunks. This is the most consequential confusion in this lesson.

Believing recursive is a distinct strategy. In current Git it is a synonym for ort.

Using -X theirs to avoid resolving. It suppresses the conflict without addressing why it occurred.

Ignoring whitespace in whitespace-significant languages. Can change behaviour silently.

Defining merge drivers in .gitattributes alone. Drivers need per-clone config.

Using union on files where order matters. Concatenation is only sane for genuinely append-only files.

Reaching for a strategy option before understanding the conflict. Nearly every everyday conflict should be resolved by reading both sides.

The strategy is which algorithm runs. The options are arguments to that algorithm.

ort is the algorithm for essentially all real merges. -X options adjust how it treats conflicts, whitespace, renames and diffs. -s ours is not really a merge algorithm at all — it is a way of recording a merge that did not happen.

  • ort is the default strategy for two-branch merges; recursive is now a synonym for it.
  • -s selects a strategy; -X passes options to one. They are not interchangeable.
  • -X ours resolves conflicting hunks in your favour; -s ours discards the other branch entirely.
  • octopus merges several branches but refuses anything requiring manual resolution.
  • Strategy options cover conflict preference, whitespace, rename detection, diff algorithm and renormalize.
  • .gitattributes plus merge drivers control merging per path; driver definitions are per-clone config.
  • Criss-cross histories produce multiple merge bases; git merge-base --all reveals them.
  • No strategy protects against semantic conflicts.

The -s ours versus -X ours difference is worth seeing once, in a repository you do not care about.

  1. Create a repository with a file f.txt containing three lines, and commit.
  2. Create side. Change line 2, and add a new file side.txt. Commit both.
  3. On main, change line 2 differently. Commit.
  4. Merge with the option: git merge -X ours side. Check line 2 (should be yours) and confirm side.txt exists.
  5. Undo: git reset --hard HEAD~1.
  6. Merge with the strategy: git merge -s ours side. Check line 2 and predict whether side.txt exists.
  7. Run git branch --merged main. Is side listed?
  8. Run git show --stat HEAD. What does the merge commit claim to have changed?

Steps 6 to 8 are the point: the file is gone, yet the history says the branch was merged. That combination is exactly why this strategy needs a deliberate reason and a commit message explaining it.

Merging preserves both histories. Rebasing rewrites one of them — which is more powerful and carries obligations that merging does not.