Skip to content

Merge Startegies


What Will We Learn?

In this post, we’re going to break down Git’s merge strategies — the different “engines” Git can use under the hood when it combines branches. By the end, you’ll know:

  • What a merge strategy actually is (and how it’s different from a merge option)
  • How recursive, octopus, resolve, and subtree strategies work
  • How to fine-tune merges using -X options like ours, theirs, and patience
  • Which strategy to reach for depending on your situation

Let’s dig in.

What Do you Mean by Merge Stragegies?

When you run git merge, you’re not just telling Git “combine these branches” — you’re also (whether you realize it or not) picking an algorithm that decides how the combining actually happens. That algorithm is the merge strategy.

Git ships with a handful of built-in strategies, and it picks one for you automatically based on how many branches you’re merging and how their history looks. You can also pick one yourself with the -s (or --strategy) flag:

git merge -s recursive feature-branch

Most of the time you’ll never need to touch this — Git’s default choice is almost always the right one. But knowing what’s happening under the hood helps a ton when a merge goes sideways and you’re staring at a wall of conflicts wondering why.

Here’s the quick mental model:

    flowchart TD
    A["git merge"] --> B{"How many branches<br/>are being merged?"}
    B -->|"2 branches,<br/>related history"| C["recursive<br/>(default)"]
    B -->|"3+ branches"| D["octopus<br/>(default for &gt;2)"]
    B -->|"Old-school,<br/>rarely used"| E["resolve"]
    B -->|"Merging in a<br/>separate project"| F["subtree"]
  

Now let’s go through each one.

Recursive Merge Stategy

recursive is the default strategy when you’re merging two branches, and it’s the one you’ve almost certainly been using without knowing it.

Here’s the problem it solves: to merge two branches, Git needs a common ancestor (the “merge base”) to compare against. Most of the time, there’s exactly one obvious common ancestor and that’s easy. But if your branches have crossed over each other multiple times (lots of merges back and forth), there can be multiple common ancestors.

The resolve strategy (below) just picks one and moves on. recursive is smarter — if it finds multiple common ancestors, it merges those ancestors together first to create a single virtual merge base, then uses that to do the real merge. It’s essentially solving the merge-base problem recursively before solving the actual merge.

    %%{init: { 'gitGraph': {'showBranches': true, 'showCommitLabel': false}} }%%
gitGraph
    commit id: "A"
    branch feature
    checkout main
    commit id: "B"
    checkout feature
    commit id: "C"
    checkout main
    commit id: "D"
    merge feature id: "recursive merge"
  

In practice, recursive also handles renamed files well and gives you decent conflict detection, which is why it became — and stayed — the default for two-branch merges since Git 1.5. In recent Git versions it’s technically been swapped out for ort (“Ostensibly Recursive’s Twin”) as the real default, which is a faster reimplementation of the same idea. You don’t need to worry about the difference day-to-day.

Octopus Merge Stategy

octopus is what Git reaches for automatically when you try to merge more than two branches at once:

git merge feature-a feature-b feature-c

Instead of doing three separate two-way merges, Git combines all of them in one shot, comparing everything against a single base:

    flowchart LR
    A["main"] --> M(["octopus merge"])
    B["feature-a"] --> M
    C["feature-b"] --> M
    D["feature-c"] --> M
    M --> E["main (merged)"]
  

It’s fast and convenient, but there’s a catch: octopus merges refuse to run if there’s a conflict. There’s no interactive conflict resolution step — if any of the branches disagree with each other, the merge just bails out and tells you to resolve things manually (usually by merging branches in one at a time instead).

Because of that, octopus merges are best suited for combining independent, non-conflicting changes — think release branches that each touched completely different files, or automated merges in CI. It’s not really a tool for day-to-day feature integration where conflicts are likely.

Resolve Merge Stategy

resolve is the older sibling of recursive. Like recursive, it only works with two branches and uses a three-way merge (common ancestor + your branch + their branch).

The difference is in how it handles multiple common ancestors: resolve doesn’t bother merging them like recursive does — it just picks one merge base (the best single one it can find) and works from there.

    flowchart TD
    subgraph Resolve["resolve strategy"]
        R1["Pick ONE common ancestor"] --> R2["Three-way merge"]
    end
    subgraph Recursive["recursive strategy"]
        C1["Multiple common ancestors?"] --> C2["Merge them into a<br/>virtual ancestor first"] --> C3["Three-way merge"]
    end
  

In practice, resolve tends to be less accurate in messy histories with multiple crossover merges, which is exactly why recursive replaced it as the default. You’ll rarely reach for resolve on purpose these days — it mostly shows up in Git’s own documentation as a “here’s what came before” reference point. Still, it’s fast and simple, so if you’re merging straightforward, linear-ish branches, it works just fine:

git merge -s resolve feature-branch

Subtree Merge Stategy

subtree is a special case: it’s for when you want to merge one repository into a subdirectory of another, treating the incoming project as if it always lived there.

A classic use case: you’ve got a library living in its own repo, and you want to pull it into your main project under vendor/library/ while still being able to pull in upstream updates later.

git remote add library-remote [email protected]:someone/library.git
git fetch library-remote
git merge -s subtree library-remote/main

Under the hood, subtree is really just recursive with an extra trick: it automatically figures out that the two trees need to be shifted relative to each other (since one repo’s root corresponds to a subdirectory in the other) before comparing them. That’s it — no black magic, just smart path adjustment.

    flowchart LR
    A["library repo<br/>(root: /)"] -->|"shifted to match"| C["main repo<br/>(vendor/library/)"]
    B["main repo<br/>(root: /)"] --> C
    C --> D["Merged history,<br/>library now lives<br/>under vendor/library/"]
  

These days, many people reach for git subtree (the separate command/contrib script) or git submodule instead, since they offer nicer workflows for ongoing syncing. But the subtree strategy is still handy for a one-time “absorb this project into mine” merge.

Merge Strategy Options (-X)

Strategies decide the overall algorithm. Strategy options (-X) let you tweak the behavior of that algorithm without switching strategies entirely. You pass them like this:

git merge -X ours feature-branch
git merge -X ignore-space-change feature-branch

These options apply to recursive (and ort) unless noted otherwise. Here are the ones you’ll actually use:

OptionWhat it does
oursWhen there’s a conflict, automatically keep your side’s changes. (Not to be confused with the whole-file -s ours strategy, which discards the other branch’s changes entirely — this only affects conflicting hunks.)
theirsSame idea, but favors the incoming branch’s changes on conflicts.
patienceUses a slower but more careful diff algorithm — great for branches with large blocks of moved or rearranged code, since it avoids the classic “diff picks the wrong matching line” mess.
diff-algorithm=<algo>Lets you pick the diff engine directly: myers (default), minimal, patience, or histogram.
ignore-space-change / ignore-all-spaceIgnores whitespace differences when comparing changes — a lifesaver when someone’s editor auto-reformatted indentation.
renormalizeRe-applies your .gitattributes line-ending/normalization rules before comparing, useful when branches disagree on line endings.
no-renames / find-renames[=<n>]Turns rename detection off, or tunes how aggressively Git tries to detect renamed files (as a similarity percentage).
subtree[=<path>]Lets you use the subtree path-shifting logic as an option on top of recursive, rather than switching to the full subtree strategy.

A quick example that combines a couple of these — useful when merging in a branch that only reformatted whitespace and you don’t want that noise to trigger conflicts:

git merge -X ignore-space-change -X patience refactor-branch

Which One to Use When?

Honestly, 95% of the time you don’t need to pick anything — Git’s defaults are good. But here’s a cheat sheet for the other 5%:

    flowchart TD
    A["Merging two branches<br/>with normal history?"] -->|Yes| B["Just use the default<br/>(recursive / ort).<br/>Don't specify -s at all."]
    A -->|No| C["Merging 3+ branches<br/>at once, low conflict risk?"]
    C -->|Yes| D["-s octopus<br/>(or just let Git pick it<br/>automatically)"]
    C -->|No| E["Pulling in a whole<br/>external project as<br/>a subdirectory?"]
    E -->|Yes| F["-s subtree"]
    E -->|No| G["Need to favor one side<br/>on conflicts, or deal with<br/>whitespace/rename noise?"]
    G -->|Yes| H["Keep -s recursive,<br/>add -X ours/theirs/<br/>ignore-space-change/etc."]
    G -->|No| I["Working with old<br/>Git tooling or docs<br/>that assume resolve?"]
    I -->|Yes| J["-s resolve"]
  

A few rules of thumb to take away:

  • Don’t fight the default. recursive/ort handles the vast majority of real-world merges correctly, including messy crossover histories.
  • Reach for octopus only when conflicts are unlikely. It’s a convenience for combining clean, independent branches — not a conflict-resolution tool.
  • subtree is for absorbing a project, not for regular merges. If you’re just merging feature branches within the same repo, you don’t need it.
  • Use -X options before reaching for a different strategy. Most “I need special merge behavior” problems are solved with an option flag on recursive, not a whole different algorithm.

Once you know what each strategy is actually doing under the hood, merge conflicts stop feeling like Git being difficult for no reason — you can usually tell exactly why it’s confused, and pick the right tool to sort it out.

Last updated on