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:
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.
- The root snapshot becomes tree
a7c4b8a...and commitcd23e8b....mainadvances from unborn to that commit. git branch featurewrites a second ref atcd23e8b.... It does not copy the commit, and HEAD remains attached tomain.- The
main-apisnapshot changes onlyapp.py. It becomes commit8602b19..., whose parent iscd23e8b.... Onlymainadvances. - HEAD switches to
feature. The branch still points at the base, sofeature-docscreates sibling commit61f01e6.... ItsREADME.mdchanged, while itsapp.pystill matches the base. - HEAD switches back to
main. The merge freezesours = 8602b19...andtheirs = 61f01e6...; neither input can move during the comparison. - The ancestry probe marks every commit reachable from ours, then walks the source history. The unique best common ancestor is
cd23e8b.... - The base differs from both tips, so this is neither “already up to date” nor a fast-forward. The engine loads all three snapshots.
- For
app.py, theirs equals base: both containprint("v1"). The third rule takes ours,print("v2"). - For
README.md, ours equals base: both containhello. The second rule takes theirs,quickstart. - The clean result is
app.py=print("v2")andREADME.md=quickstart. Git serializes it as tree2bcb0a8.... - The merge commit body names that tree, then writes parent 1 as old HEAD
8602b19...and parent 2 as source tip61f01e6.... The stored commit is7614647.... - Finally,
mainadvances to7614647....featureremains at61f01e6.... 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, git merge-base, and the index-stage description in git read-tree.
Keep exploring
A few lessons that share the same technique or lead into the next idea.
Found a mistake or something unclear? Leave feedback.