Skip to main content
RunBook Academy

Git, CI/CD & GitOpsIX · MergingMerging

Merge strategies — recursive, resolve, octopus, ours, subtree

Intermediate⏱ ~24 mingit

What you'll learn

  • List the five built-in merge strategies and the topology each is suited to
  • Explain why recursive is the default for two-parent merges and where it differs from resolve
  • Use --strategy-option to apply patience, diff-algorithm, or whitespace-tolerance options
  • Recognise when octopus is appropriate (many-branch release merges) and when it refuses (conflict)
  • Choose between ours, subtree, and recursive for the common infrastructure-repository cases

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

The merge strategy is the algorithm that turns three tree inputs into a merged tree. Git ships with five built-in strategies, each suited to a different topology and a different operational intent. Choosing a strategy is rarely required — recursive is the right default for almost every two-parent merge — but the operational cases where the other strategies matter (octopus for release branches, subtree for sub-projects, ours for intentional subsumption) are common enough in infrastructure repositories that the right strategy needs to be a deliberate choice, not a forgotten flag.

The five strategies

The five strategies and their operational purpose:

# 1. recursive — default for two-parent merges; handles criss-cross
git merge feature/iam-rotation
# Merge made by the 'recursive' strategy.

# 2. resolve — historical default; does not handle criss-cross
git merge --strategy=resolve feature/iam-rotation
# Merge made by the 'resolve' strategy.

# 3. octopus — many-branch merge; refuses if any path conflicts
git merge --strategy=octopus branch1 branch2 branch3
# Merge made by the 'octopus' strategy.

# 4. ours — declare current branch supersedes other; discard other side's changes
git merge --strategy=ours feature/deprecated
# Merge made by the 'ours' strategy.

# 5. subtree — merge a project that lives at a different root path
git merge --strategy=subtree --prefix=vendor/$NAME $VENDOR_BRANCH
# Merge made by the 'subtree' strategy.

The strategy can be set per-command with --strategy (or the short form -s), or configured at the repository level with merge.$NAME.driver for custom strategies, or at the path level with .gitattributes for path-specific drivers.

flowchart LR
    A["git merge"] --> B{Strategy?}
    B -->|"default (recursive)"| C["two-parent merge\nrecursive"]
    B -->|"--strategy=octopus"| D["many-branch merge\noctopus"]
    B -->|"--strategy=ours"| E["supersede branch\nours"]
    B -->|"--strategy=subtree"| F["sub-project merge\nsubtree"]
    B -->|"--strategy=resolve"| G["legacy two-parent\nresolve"]

The decision tree is shallow: most merges use the default; octopus and ours are explicit choices; subtree is an integration pattern (vendor branches); resolve is a legacy choice that rarely wins.

Recursive — the default

recursive is the strategy that handles every two-parent merge case correctly, including the criss-cross case where multiple merge bases exist. It is described in detail in IX-02; the operational summary is:

# Default: no flag needed
git merge feature/iam-rotation
# Merge made by the 'recursive' strategy.

# Explicit, for clarity in scripts
git merge --strategy=recursive feature/iam-rotation
# Merge made by the 'recursive' strategy.

The strategy finds all merge bases, recursively merges them into a virtual base, and then runs the per-path three-way merge against the virtual base. This is what makes recursive safe for repositories with criss-crossed history — release branches that have been merged into each other over the lifecycle of a release.

The --strategy-option flag modifies the strategy’s behaviour. The most commonly useful options for infrastructure code:

# patience: prefer fewer-but-larger diff hunks; better for
# files that have been heavily refactored
git merge --strategy-option=patience feature/iam-rotation

# diff-algorithm=histogram: variant of patience; faster
git merge --strategy-option=diff-algorithm=histogram feature/iam-rotation

# ignore-all-space: tolerate whitespace-only differences;
# useful when one side has been auto-formatted
git merge --strategy-option=ignore-all-space feature/iam-rotation

# renormalize: re-detect CRLF/LF on each blob; useful when
# branches have different line-ending settings
git merge --strategy=recursive --strategy-option=renormalize feature/x

The patience option is particularly valuable for YAML files (Kubernetes manifests, Ansible playbooks, GitHub Actions workflows) where reformatting can produce long chains of small diffs that confuse the default Myers diff algorithm. Switching to patience often produces a smaller, more meaningful conflict set.

Resolve — the legacy strategy

resolve was the default strategy before Git learned the recursive strategy in version 1.5.6 (2007). It performs the three-way merge using a single merge base, choosing one arbitrarily if multiple bases exist. For a simple forked graph (one merge base), resolve and recursive produce identical results. For a criss-crossed graph (multiple bases), resolve can pick a non-representative base and miss conflicts that recursive would catch.

git merge --strategy=resolve feature/iam-rotation
# Merge made by the 'resolve' strategy.

There is essentially no operational reason to choose resolve over recursive in modern Git. The only argument for resolve is “I want the older, simpler behaviour and my graph is guaranteed not to criss-cross”. That guarantee is hard to make for any repository with release branches. The default is right; reach for resolve only when debugging a difference between recursive and resolve behaviour, which is itself a sign that the history has unexpected structure.

Octopus — many-branch merges

octopus is the strategy for merging more than two branches in a single commit. It applies the three-way merge sequentially across all branches and refuses to proceed if any path conflicts:

git merge --strategy=octopus branch1 branch2 branch3
# Merge made by the 'octopus' strategy.
#  terraform/a.tf   | 2 +-
#  terraform/b.tf   | 4 ++--
#  terraform/c.tf   | 6 +++---

The resulting merge commit has more than two parents (one per branch being merged in, plus the original HEAD). The graph encodes the topology of a many-branch convergence in a single commit.

The cases where octopus is the right choice:

  • Release merges. A release branch landing several feature branches at once. The octopus commit is the unit of release, and the audit trail “what landed in release/2026-q3?” is a single commit with N parents.
  • Bulk dep updates. Several renovate/dependabot PRs landing together. An octopus merge produces one commit for the bulk update rather than N fast-forwards.
  • Stable topic bundles. A stable topic branch (e.g. stable/lts) that is supposed to receive a curated set of fixes; the octopus commit records the curation.

The cases where octopus is the wrong choice:

  • The branches touch the same paths. Octopus refuses to proceed if any path conflicts. The refusal is the intended behaviour — octopus is for clean, non-overlapping convergences. If your branches overlap, run them as separate merges.
  • Audit by branch lifecycle is required. An octopus commit has N parents, and git log --first-parent only shows the octopus commit itself; the individual feature branches are reachable but not on the first-parent chain. For “show me what landed”, --first-parent is not enough; the engineer must walk all parents.

Ours — supersede without taking

ours is the strategy that declares the current branch supersedes the merged-in branch without taking any of the merged-in branch’s changes. The result is a merge commit whose tree is identical to the current branch’s tree, with the merged-in branch’s tip as the second parent.

git merge --strategy=ours feature/deprecated
# Merge made by the 'ours' strategy.
#  (no file changes)

The commit is structurally a merge commit — it has two parents — but the tree matches the current branch. The operational purpose: record “we acknowledged this branch and chose not to take its changes” in the graph without discarding the branch.

The cases where ours is the right choice:

  • Deprecation. A branch whose work has been superseded by another approach should be merged with ours to record the supersession in the graph. The branch tip is reachable from the merge commit, so the history is preserved; the working tree of the trunk does not change.
  • Policy gates. A branch that failed a policy check (security review, license check) but that the team wants to record as “considered, rejected”. The ours merge is the record of the consideration.
  • Topic branch cleanups. A batch of stale feature branches that the team wants to mark as “absorbed” or “abandoned” without losing their history.

The warning: ours is dangerous if the engineer’s intent is “merge in the other branch’s changes” — the strategy discards the other branch’s work. Double-check the flag and the intent before using it.

Subtree — sub-project integration

subtree is the strategy for merging a project that lives at a different root path. The canonical case is a vendor branch: a third-party project maintained in its own branch that the team wants to integrate at a sub-directory of the main repository.

# Initial merge: bring the vendor project into a sub-directory
git merge --strategy=subtree --prefix=vendor/lib $VENDOR_BRANCH
# Merge made by the 'subtree' strategy.
#  vendor/lib/file.c | 100 +++++++++++++++
#  1 file changed, 100 insertions(+)

# Subsequent merges: bring in upstream changes to the same path
git merge --strategy=subtree --prefix=vendor/lib $VENDOR_BRANCH
# Merge made by the 'subtree' strategy.

The strategy works by detecting the prefix relationship between the merged-in branch’s root and the target path, and adjusting the three-way merge accordingly. The first merge establishes the prefix; subsequent merges use the same prefix to align the trees.

The cases where subtree is the right choice:

  • Vendor branches. The team tracks a third-party project (a vendored library, an upstream Terraform module) in its own branch and periodically merges it into the main repository at a sub-directory. subtree is the cleanest way to do this without the git submodule machinery.
  • Sub-project integration. A repository that has graduated from being a sub-directory of a larger project to being its own project, where the team wants to keep an integration path available for historical merges.
  • Split repositories. A repository that was split out of a larger repository, where the team wants to pull specific paths from the larger repository into the split.

Production discipline

Three rules for merge strategies in a production-grade workflow:

  1. Use the default unless you have a specific reason not to. The recursive strategy is the right choice for every two-parent merge in a modern Git workflow. Override only for octopus (many-branch), ours (supersede), or subtree (vendor).
  2. Configure YAML-friendly options at the repo level. merge.conflictStyle = diff3 and merge.algorithm = patience in .git/config make every YAML merge produce better conflict markers and better diff hunks without per-command flags.
  3. Document octopus merges in the commit message. An octopus commit’s parents are the branches it absorbed; the commit message should name them. The default message from git merge --strategy=octopus does this, but a custom -m should also list the branches.

Cross-course references

  • Linux for Production Sysadmins - Parts XII (RepoSecurity) covers package-source integration; subtree is the Git-level analogue of integrating upstream sources at a non-root path.
  • Ansible for Production Sysadmins - Part XXXVII (RepoArch) covers role collection; octopus merges are the natural fit for “absorb N role updates into a release branch”.
  • Terraform for Production Sysadmins - Parts IX-XII (State) cover Terraform module sources; subtree is the Git-level analogue of vendoring a Terraform module at a sub-path.

Quiz

Knowledge check · 4 questions

  1. Q1. Which merge strategy is the right choice for declaring that a branch has been superseded without taking any of its changes into the current branch?

  2. Q2. The `octopus` strategy refuses to proceed if any path in the merged-in branches conflicts.

  3. Q3. Name the `git merge` flag that applies a strategy option to the recursive strategy, and give one example of a useful option for YAML-heavy repositories.

  4. Q4. Recommend a merge strategy for a release-branching workflow that absorbs three feature branches at once.

    Your team maintains a `release/2026-q3` branch. Three feature branches — `feature/iam-rotation`, `feature/network-fixes`, `feature/observability-tweaks` — have been reviewed and are ready to land together. The branches touch disjoint paths (no overlapping files), and the team wants a single commit on `release/2026-q3` that records the absorption of all three.

Passing score: 75%. Answers are checked in this browser.