Git Merge and Merge Strategies Explained

Deep dive into Git merge strategies — fast-forward, three-way, recursive, ours, subtree. Learn when each strategy applies and how to control merge behavior.

published: reading time: 12 min read updated: March 31, 2026

Introduction

Merging is how Git combines divergent histories. When two branches have taken different paths, a merge reconciles those paths into a single coherent history. Git provides multiple merge strategies, each suited to different scenarios and team workflows.

Understanding merge strategies isn’t academic — it directly affects your project’s history readability, conflict frequency, and release stability. The wrong strategy can produce tangled histories or silently discard work. The right strategy keeps your history clean and your team confident.

Git’s default strategy handles most everyday cases automatically. But when things get complex — subtree merges, vendor branches, or intentional divergence — knowing your options makes the difference between a clean integration and a debugging nightmare.

When to Use / When Not to Use

When to Use Merge

  • Integrating completed features — bring finished work into the main line
  • Combining parallel development — reconcile work done by different team members
  • Release integration — merge release candidates back into main after stabilization
  • Preserving history — merge commits record when and how branches were combined
  • Team collaboration — pull requests use merge under the hood

When Not to Use Merge

  • Linear history preference — use rebase instead if you want a straight-line history
  • Before pushing shared work — never rebase after pushing, but merge is safe
  • Trivial changes — cherry-pick individual commits instead of merging entire branches
  • When branches have diverged significantly — consider interactive rebase for cleaner history

Core Concepts

Git uses different merge strategies depending on the relationship between branches. The strategy determines how Git computes the combined result.


graph TD
    Merge["git merge"] --> FF{"Can fast-forward?"}
    FF -->|Yes| FastForward["Fast-Forward Merge"]
    FF -->|No| ThreeWay{"Common ancestor?"}
    ThreeWay -->|Yes| Recursive["Recursive Strategy (default)"]
    ThreeWay -->|No| Octopus["Octopus / Orphan strategy"]
    Recursive --> Conflict{"Conflicts?"}
    Conflict -->|Yes| Resolve["Manual Resolution"]
    Conflict -->|No| AutoMerge["Automatic Merge Commit"]
    FastForward --> NoCommit["No merge commit created"]

Fast-Forward Merge

When the target branch hasn’t moved since the feature branch was created, Git simply moves the pointer forward. No merge commit is created.


Before:
main:    A ── B
                   \
feature:              C ── D

After (fast-forward):
main:    A ── B ── C ── D
feature:              └────── (same commits)

Three-Way Merge (Recursive)

When both branches have diverged, Git finds the common ancestor and creates a new merge commit that combines both lines of work.


Before:
main:    A ── B ── E
           \
feature:      C ── D

After (three-way merge):
main:    A ── B ── E ── M (merge commit)
           \         /
feature:      C ── D ─┘

Architecture or Flow Diagram


flowchart LR
    A["main: A-B-E"] --> Check{"Feature branch\nexists?"}
    Check -->|Feature is\nancestor of main| FF["Fast-Forward\nNo merge commit"]
    Check -->|Both diverged| ThreeWay["Three-Way Merge\nFind merge base"]
    ThreeWay --> Auto{"Auto-resolvable?"}
    Auto -->|Yes| MC["Create Merge Commit\n2 parents"]
    Auto -->|No| Conflict["CONFLICT\nManual resolution required"]
    Conflict --> MC
    FF --> Done["Done"]
    MC --> Done

Step-by-Step Guide / Deep Dive

Basic Merge


# Switch to the target branch
git switch main

# Merge the feature branch into main
git merge feature-x

# Merge with a custom message
git merge feature-x -m "Integrate feature-x: payment processing"

Controlling Merge Strategy


# Force a merge commit even if fast-forward is possible
git merge --no-ff feature-x

# Force fast-forward only (fail if not possible)
git merge --ff-only feature-x

# Use 'ours' strategy (keep current branch, discard other)
git merge -s ours feature-x

# Use recursive with specific conflict resolution preference
git merge -s recursive -X ours feature-x
git merge -s recursive -X theirs feature-x

Merge Strategies Explained

resolve — The original three-way merge algorithm. Can only handle two branches. Faster but less sophisticated than recursive.


git merge -s resolve feature-x

recursive — The default strategy for two branches. Handles renames, detects criss-cross merges, and provides better conflict resolution.


git merge -s recursive feature-x

octopus — Merges more than two branches simultaneously. Default for merging 3+ branches.


git merge feature-a feature-b feature-c  # auto-uses octopus

ours — Records a merge but keeps all content from the current branch. Useful for deprecating branches.


git merge -s ours deprecated-branch

subtree — Merges a subproject with its own history into a subdirectory. Useful for vendoring dependencies.


git merge -s subtree --squash -m "Add vendor lib" vendor-lib

Merge Options


# Squash all commits into a single commit (no merge commit)
git merge --squash feature-x
git commit -m "Add feature-x"

# Abort a merge in progress
git merge --abort

# Continue a merge after resolving conflicts
git merge --continue

# Show what would be merged without actually merging
git merge --no-commit --no-ff feature-x

Production Failure Scenarios + Mitigations

ScenarioImpactMitigation
Accidental fast-forward loses merge contextHistory doesn’t show feature integrationUse --no-ff for feature branches
Merge conflict in binary filesUnresolvable without manual interventionAvoid committing binaries; use Git LFS
Wrong merge strategy discards workData loss from ours strategyAlways review before using -s ours
Criss-cross merge confusionGit picks wrong merge baseUse recursive strategy; it handles this
Merge during CI failureBroken main branchRequire CI to pass before merge

Recovering from a Bad Merge


# If merge is still in progress
git merge --abort

# If merge was already committed
git reset --hard HEAD~1  # removes the merge commit

# If already pushed, create a revert
git revert -m 1 <merge-commit-sha>

Trade-offs

StrategyProsCons
Fast-forwardClean linear historyLoses context about when feature was integrated
--no-ff mergePreserves feature branch context in historyCreates extra merge commits, busier history
--squashSingle clean commit on targetLoses individual commit history of the feature
oursClean way to deprecate branchesDiscards all changes from merged branch
subtreeKeeps subproject historyComplex to manage updates
recursive -X theirsAuto-resolves conflicts in favor of incomingMay silently overwrite important changes

Implementation Snippets


# Standard feature branch merge (preserves context)
git switch main
git pull origin main
git merge --no-ff feature-x
git push origin main

# Squash merge for small features
git switch main
git merge --squash feature-x
git commit -m "feat: add user profile validation"

# Merge with conflict preference (use carefully)
git merge -s recursive -X ours experimental

# Subtree merge for vendored library
git remote add -f lib-vendor https://github.com/vendor/lib.git
git merge -s subtree --no-commit lib-vendor/main
git commit -m "Add vendor/lib at v2.3.0"

Observability Checklist

  • Logs: Record merge commits with descriptive messages linking to issue IDs
  • Metrics: Track merge frequency and conflict rate per team
  • Alerts: Alert on merge conflicts in protected branches
  • Traces: Link merge commits to PR numbers for audit trails
  • Dashboards: Display merge-to-deploy lead time in CI/CD dashboards

Security/Compliance Notes

  • Merge commits should reference issue/PR numbers for audit compliance
  • Protected branches should require approved PRs before merging
  • Review merge diffs carefully — --squash can hide problematic individual commits
  • Use signed merge commits (git merge -S) for supply chain security
  • Audit ours strategy usage — it can silently discard security fixes

Common Pitfalls / Anti-Patterns

  • Merging without pulling first — creates unnecessary merge commits; always pull before merging
  • Using --squash for large features — loses valuable commit history and makes bisecting harder
  • Ignoring merge conflicts — resolving conflicts by accepting everything from one side can break functionality
  • Merging into the wrong branch — double-check your current branch before running git merge
  • Forgetting --no-ff — fast-forward merges lose the visual grouping of feature commits in history
  • Merge commit spam — merging tiny branches creates noise; batch small changes together

Quick Recap Checklist

  • Fast-forward merges move the pointer without a merge commit
  • Three-way merges create a merge commit with two parents
  • Use --no-ff to preserve feature branch context
  • Use --squash for combining many commits into one
  • Use -s ours to record a merge while keeping current content
  • Use -s subtree for vendoring external projects
  • Always pull before merging to avoid unnecessary conflicts
  • Use git merge --abort to cancel a merge in progress

Interview Q&A

What is the difference between git merge --squash and git merge --no-ff?

--squash combines all changes into a single commit with no merge commit and no parent references — the individual commit history is lost. --no-ff creates a merge commit with two parents, preserving both the feature branch history and the integration point in the graph.

What does the ours merge strategy do, and when would you use it?

The ours strategy creates a merge commit that keeps all content from the current branch and discards all changes from the merged branch. Use it to deprecate a branch — for example, when retiring an old release branch and recording that it was intentionally superseded.

How does Git detect and handle rename conflicts during a merge?

The recursive strategy tracks file renames by comparing content similarity (not just filenames). If one branch renames a file and the other modifies it, Git detects the rename and applies the modifications to the new filename. If both branches rename the same file differently, it creates a rename/rename conflict requiring manual resolution.

What is a criss-cross merge and how does Git handle it?

A criss-cross merge occurs when two branches have been merged into each other multiple times, creating multiple common ancestors. Git's recursive strategy handles this by performing a virtual merge base — it recursively merges the common ancestors to find the best base for the actual merge.

Merge Strategy Visualization


graph TD
    subgraph "Fast-Forward Merge"
        A1["A"] --> B1["B"]
        B1 --> C1["C"]
        C1 --> D1["D"]
        D1 -. "main moves here" .-> D1
    end

    subgraph "Three-Way Merge (Recursive)"
        A2["A"] --> B2["B"]
        B2 --> E2["E"]
        A2 --> C2["C"]
        C2 --> D2["D"]
        E2 --> M2["M (merge commit, 2 parents)"]
        D2 --> M2
    end

    subgraph "Squash Merge"
        A3["A"] --> B3["B"]
        B3 --> C3["C"]
        A3 --> D3["D"]
        D3 --> E3["E"]
        E3 --> S3["S (single commit, no parents)"]
    end

    classDef ff fill:#16213e,color:#00fff9
    class A1,B1,C1,D1,A2,B2,C2,D2,E2,M2,A3,B3,C3,D3,E3,S3 ff

Production Failure: Accidental Squash Merge Losing History

Scenario: A team lead merges a feature branch with 15 carefully crafted commits using git merge --squash. The squash creates a single commit with all changes combined. Two weeks later, a bug is traced to one specific change within that feature. git bisect can’t isolate the problematic commit because individual commit history was destroyed.

Impact: Lost ability to bisect, audit individual changes, or revert specific parts of the feature. The entire feature must be reverted as a unit.

Mitigation:

  • Reserve --squash for small, atomic features (1-3 commits)
  • Use --no-ff for features with meaningful commit history
  • Document squash merges in PR descriptions with commit summaries
  • Configure branch protection to require merge commits for regulated code

# Check if a merge was squashed (single commit with multiple file changes)
git log --oneline --since="2 weeks ago" | head -20
git show <commit> --stat  # if too many files, likely a squash

Trade-offs: Merge Strategies

StrategyHow It WorksBest ForRisks
Fast-forwardMoves pointer forward, no merge commitLinear feature branches, solo developmentLoses integration context, can’t identify when feature was merged
Recursive (default)Three-way merge with merge commitMost team workflows, diverged branchesCreates merge commits that clutter history
OursKeeps current branch content, discards otherDeprecating branches, resolving deprecationSilently discards all incoming changes — dangerous
SubtreeMerges subproject into subdirectoryVendoring dependencies, subproject managementComplex to update, requires subtree-specific knowledge
OctopusMerges 3+ branches simultaneouslyCombining multiple independent featuresCan’t resolve conflicts, auto-aborts on conflict
ResolveOriginal three-way algorithmSimple two-branch mergesLess sophisticated than recursive, doesn’t handle renames well

Security/Compliance: Merge Commit Signatures

In regulated environments, merge commits should be cryptographically signed to verify who authorized the integration:


# Sign a merge commit
git merge -S feature-x

# Verify merge commit signatures
git log --show-signature --merges

# Configure automatic signing for all merges
git config --global commit.gpgSign true
git config --global merge.gpgSign true

# Verify the entire merge chain
git verify-commit <merge-commit-sha>

Compliance notes:

  • Signed merge commits create an auditable chain of who approved each integration
  • Required for SOC 2, HIPAA, and financial industry compliance
  • Platform-level merge (GitHub/GitLab UI) may not preserve GPG signatures — use CLI for signed merges
  • Document merge signature verification in your security policy
  • Audit unsigned merge commits in protected branches

Resources

Category

Related Posts

Rebase vs Merge: When to Use Each in Git

Decision framework for choosing between git rebase and git merge. Understand trade-offs, team conventions, history implications, and production best practices.

#git #version-control #rebase

Git Remote Management: Adding, Removing, and Configuring Remotes

Master git remote operations — adding, removing, renaming remotes, managing multiple remotes, and configuring remote URLs for effective collaboration.

#git #version-control #remote

Pull Requests and Code Review: Git Collaboration Best Practices

Master pull request workflows and code review — writing effective PR descriptions, review best practices, collaboration patterns, and team workflows.

#git #version-control #pull-requests