---
title: "Three-Way Merge"
description: "Combine two diverged histories through their common ancestor and a two-parent commit."
source: https://vizlearn.app/cs/version-control/three-way-merge
---

# Three-Way Merge

A three-way merge combines two branch tips by comparing them with a shared earlier snapshot. Git calls the checked-out tip **ours**, the named source tip **theirs**, and their best common ancestor the **merge base**. The base is what turns a two-sided disagreement into a question Git can often answer: which side changed, and which side stayed put?

This lesson separates two jobs that are easy to blur together:

- The commit graph chooses the merge base and classifies the relationship between the tips.
- The three snapshots decide the result, one path at a time.

Only after every path has a clean result does the engine write a merged tree, store a commit with two parent lines, and advance the current branch. If a path conflicts, none of those writes happen.

## The four path rules

For one path, let `base`, `ours`, and `theirs` be the file values in the three snapshots. The scoped merge uses four rules, in this order:

| Condition        | Result                                       |
| ---------------- | -------------------------------------------- |
| `ours == theirs` | Take either value. Both sides agree.         |
| `ours == base`   | Take `theirs`. Only the source side changed. |
| `theirs == base` | Take `ours`. Only the current side changed.  |
| All three differ | Stop with a conflict.                        |

The order matters. If both branches independently reach the same value, the first rule accepts it even when that value differs from base.

These are deliberately small rules for two flat text files. Production Git also handles directories, file modes, deletions, renames, binary files, merge drivers, and strategy options. The role of the base does not change: it supplies the before-state for both sides.

## Step by Step

The default run executes this operation trace:

```text
commit snapshot "base":         app.py=print("v1"), README.md=hello
git branch feature
commit snapshot "main-api":     app.py=print("v2"), README.md=hello
git switch feature
commit snapshot "feature-docs": app.py=print("v1"), README.md=quickstart
git switch main
git merge feature
```

The object IDs are deterministic Git-format SHA-1 IDs. Fixed identity and time fields keep the lesson focused on trees and parents.

1. The root snapshot becomes tree `a7c4b8a...` and commit `cd23e8b...`. `main` advances from unborn to that commit.
2. `git branch feature` writes a second ref at `cd23e8b...`. It does not copy the commit, and HEAD remains attached to `main`.
3. The `main-api` snapshot changes only `app.py`. It becomes commit `8602b19...`, whose parent is `cd23e8b...`. Only `main` advances.
4. HEAD switches to `feature`. The branch still points at the base, so `feature-docs` creates sibling commit `61f01e6...`. Its `README.md` changed, while its `app.py` still matches the base.
5. HEAD switches back to `main`. The merge freezes `ours = 8602b19...` and `theirs = 61f01e6...`; neither input can move during the comparison.
6. The ancestry probe marks every commit reachable from ours, then walks the source history. The unique best common ancestor is `cd23e8b...`.
7. The base differs from both tips, so this is neither “already up to date” nor a fast-forward. The engine loads all three snapshots.
8. For `app.py`, theirs equals base: both contain `print("v1")`. The third rule takes ours, `print("v2")`.
9. For `README.md`, ours equals base: both contain `hello`. The second rule takes theirs, `quickstart`.
10. The clean result is `app.py=print("v2")` and `README.md=quickstart`. Git serializes it as tree `2bcb0a8...`.
11. The merge commit body names that tree, then writes parent 1 as old HEAD `8602b19...` and parent 2 as source tip `61f01e6...`. The stored commit is `7614647...`.
12. Finally, `main` advances to `7614647...`. `feature` remains at `61f01e6...`. The merge adds one commit object, writes the current ref once, and never writes the source ref.

The parent order records the command's point of view. Parent 1 is the pre-merge current tip; parent 2 is the tip named on the merge command. Swapping those lines while keeping the same result tree would still produce a different commit ID because the commit hash covers the serialized parent headers.

## Why the merge base must be the best one

The first common ancestor found during a walk is not automatically the right base. A common ancestor can have a newer common descendant. In that case the older commit is still shared history, but it is not a best candidate.

The scoped engine collects all common ancestors, then removes any candidate that is an ancestor of another common candidate. Ordinary split histories leave one answer. Some criss-cross histories leave several best candidates. This visualizer stops at that boundary; Git's default `ort` strategy can merge multiple bases into a virtual base before it merges the branch tips.

That distinction prevents an older snapshot from being treated as the before-state when a more recent shared snapshot exists. The path rules are only as trustworthy as the base supplied to them.

## Edge Cases

### Merge base equals ours

The current tip is an ancestor of the source tip. Git can fast-forward by moving the current ref to the existing source commit. No snapshot reconciliation or merge commit is needed. The workbench stays closed because there are not three distinct roles to compare.

### Merge base equals theirs

The current history already contains the source tip. Git reports “already up to date.” There are zero object writes and zero ref writes, even when the two branch names point to different commits.

### Both sides make the same edit

If ours and theirs contain the same value for a path, the first rule takes it. The base may differ. Agreement at the tips is enough for that path.

### Both sides change one path differently

When base, ours, and theirs are pairwise different, the scoped engine stops before writing the result tree. Real Git records the three versions in index stages 1, 2, and 3 and writes merge-state files such as `MERGE_HEAD`; the working tree may contain conflict markers for a text merge. A merge commit appears only after the conflict is resolved and committed.

### More than one best merge base

Criss-cross merges can produce multiple best common ancestors. The animation labels this as a model boundary rather than pretending the history is invalid. Git's recursive-style strategies can synthesize a virtual merge base.

### Eight displayed commits

Eight is a layout cap for this visualizer, not a Git limit. The trace emits a scope verdict and leaves the proposed commit unstored, so the graph never implies that real Git rejects a larger repository.

## Common Mistakes

**“Three-way means three branches.”** The three inputs are snapshots: base, ours, and theirs. Only two branch tips participate in the command.

**“The merge base is whichever common ancestor the walk reaches first.”** Git needs a best common ancestor. An older shared commit is discarded when a newer shared candidate descends from it.

**“Ours always wins.”** Ours wins only when theirs still matches base. If ours stayed unchanged, the clean rule takes theirs. If both sides agree, their shared value wins regardless of base.

**“A conflict creates a special conflict commit.”** It does not. The merge pauses with unresolved index state. The eventual commit is created only after every path has a resolved result.

**“Both branch refs move after a merge.”** Only the checked-out destination branch advances. The source ref remains where it was.

**“The merge commit stores a patch.”** It names one complete result tree and two parent commits. Diffs are views computed between those snapshots.

**“Parent order is cosmetic.”** Parent lines are ordered bytes in the commit body. Changing their order changes the object ID and changes which parent commands such as first-parent history traversal treat as the mainline.

## Scope of the model

The animation models a local clean repository with attached HEAD, two flat `100644` files, deterministic SHA-1 objects, up to three branch refs, and one unique best merge base. Its path merge compares whole string values. It omits the working tree and index during clean merges, rename detection, deletions, file modes, binary drivers, custom attributes, hooks, signatures, tags, remotes, submodules, octopus merges, strategy options, reflogs, concurrent ref updates, and recursive virtual-base construction.

For production behavior, see [`git merge`](https://git-scm.com/docs/git-merge), [`git merge-base`](https://git-scm.com/docs/git-merge-base), and the index-stage description in [`git read-tree`](https://git-scm.com/docs/git-read-tree).
