
How Git Merging Works — Three-Way Merge, Fast-Forward, and Conflicts
Branching is free. Merging is where the work happens. Git provides several strategies for combining divergent histories — fast-forward, three-way merge, squash, and rebase. Understanding which one Git chooses and why explains most of the surprises developers encounter.
Fast-Forward Merge
The simplest case: the branch you are merging into has not moved since the branch was created. The histories have not diverged — the feature branch is strictly ahead.
git checkout main
git merge feature
If main points to an ancestor of feature, Git does not create a merge commit. It moves the main pointer forward to the same commit as feature. This is a fast-forward — no new commit, no new object, just a ref update.
Fast-forward is the default when it is possible. It keeps history linear. Many teams enforce it with --ff-only to guarantee a clean commit line.
Three-Way Merge
When both branches have new commits, fast-forward is not possible. Git performs a three-way merge using three inputs:
- Merge base — the most recent common ancestor of both branches. Git finds this by walking the DAG backwards from both branch tips until the paths converge.
- Ours — the current branch tip (where HEAD points).
- Theirs — the branch being merged in.
For each file, Git compares the merge base version to both tips:
- If only one side changed the file, take that side's version.
- If neither side changed the file, keep the merge base version.
- If both sides changed the file but in different regions, combine both changes.
- If both sides changed the same lines, that is a conflict — Git cannot decide automatically.
The result is a new merge commit with two parents — one pointing to each branch tip. The merge commit's tree represents the combined state.
How Conflicts Are Detected
A conflict occurs when both branches modify the same lines relative to the merge base. Git works at the line level:
- Diff the merge base against ours — find what we changed.
- Diff the merge base against theirs — find what they changed.
- If both diffs touch the same line range, Git cannot automatically resolve it.
Git marks the conflict in the file with conflict markers:
<<<<<<< HEAD
our version of the line
=======
their version of the line
>>>>>>> feature
You resolve conflicts by editing the file to the desired state, removing the markers, and running git add to mark it resolved. Then git commit completes the merge.
Conflicts are not errors. They are the correct behavior when two humans change the same code. The merge algorithm is doing its job by refusing to guess.
Merge Strategies in Practice
Merge commit (git merge feature) — preserves the full branch topology. The merge commit has two parents. You can see exactly when the branch diverged and when it was integrated. History is non-linear but complete.
Squash merge (git merge --squash feature) — takes all changes from the feature branch and stages them as a single new commit on the target branch. No merge commit, no branch history. The feature branch's individual commits are lost in the main line. Useful for keeping main's history clean when the branch had messy intermediate commits.
Fast-forward (git merge --ff-only feature) — only succeeds if the merge is a fast-forward. Ensures linear history. Fails if the branches have diverged, forcing you to rebase first.
Each strategy has tradeoffs:
| Strategy | History | Branch topology | Bisect-friendly |
|---|---|---|---|
| Merge commit | Non-linear | Preserved | Yes (two parents) |
| Squash | Linear | Lost | Yes (one commit) |
| Fast-forward | Linear | Implicit | Yes |
Recursive and ORT Merge Strategies
When Git performs a three-way merge and the merge base is ambiguous (multiple common ancestors — common in long-lived branches), it uses the recursive strategy (or ort in Git 2.33+). Recursive merge merges the merge bases first to create a virtual merge base, then uses that for the final three-way merge.
You rarely need to think about this, but it explains why some merges that seem simple can produce surprising conflicts — the virtual merge base may differ from what you expect.
Octopus Merges
A merge can have more than two parents. An octopus merge merges three or more branches simultaneously:
git merge branch-a branch-b branch-c
Git creates a single commit with three parent pointers. Octopus merges only work when there are no conflicts — Git will not attempt conflict resolution with more than two sources. They are used by the Linux kernel to merge multiple topic branches that are known to be independent.
When Merges Go Wrong
Most merge problems come from one of three causes:
- Long-lived branches — the longer a branch lives, the more the merge base diverges from both tips. More divergence means more potential conflicts.
- Refactored code — if one branch renames a function and another branch modifies the same function, Git sees changes to different files (or different regions) and may merge cleanly but produce broken code. Merging preserves text, not semantics.
- Binary files — Git cannot diff or merge binary files. Both-sides-changed always conflicts. Use Git LFS or lock-based workflows for binary assets.
The solution to all three: merge often, keep branches short-lived, and run tests after merging.
Next Steps
- How Git Rebase Works — an alternative to merging that rewrites history.
- How Git Branching Works — the ref model that makes all of this possible.
- How Trees Work — the data structure behind directory snapshots in each commit.