| English | 中文 | Interactive demo | Runnable lab |
Two people start from the same code. Why does Git need a common ancestor, and why does one merge merely move a branch ref while another creates a commit? Comparing only the two branch tips is insufficient. Git also needs the snapshot from which the histories diverged so that it can determine what each side changed.
This chapter answers five questions:
base, ours, and theirs mean in a regular merge?Let B be the common ancestor of the current branch and the branch being merged:
F1---F2 feature
/
...---B---M1---M2 main (HEAD)
A regular three-way merge compares three snapshots:
base = B the version both sides inherited before divergence
ours = M2 the current HEAD version
theirs = F2 the version from the branch being merged
Git uses the base -> ours and base -> theirs changes to construct a result. Read the common ancestor directly with:
git merge-base main feature
Two commits can have multiple equally good common ancestors, such as after criss-cross merges. git merge-base --all lists every candidate. Without --all, do not treat one printed object ID as the sole possible candidate. For an everyday two-branch merge, establish that the reported commit is reachable from both sides.
base, ours, and theirs in a regular mergeDuring a content conflict, Git retains more than working-tree conflict markers. The index temporarily stores three versions:
| Index stage | Content source | Meaning in the graph |
|---|---|---|
| 1 | merge base | B |
| 2 | current HEAD |
ours, or M2 |
| 3 | MERGE_HEAD |
theirs, or F2 |
Inspect them while a regular merge is still in progress:
git status --short
git merge-base HEAD MERGE_HEAD
git ls-files --unmerged
git show :1:conflict.txt
git show :2:conflict.txt
git show :3:conflict.txt
The :1:, :2:, and :3: forms read objects from the index. They complement the conflict markers in the working tree. After editing the working-tree file, run:
git add conflict.txt
git merge --continue
git add replaces the three higher-stage entries with a stage-0 entry containing the resolution. The merge can create its result commit only after every conflicted path returns to stage 0.
Chapter 03 introduced the index as the next commit draft. Here it has a second role during a conflict: it preserves the three candidate inputs to the merge. Return to the Index chapter.
When the current branch is an ancestor of the branch being merged:
main: B
feature: B---F1---F2
Git can move main directly to the existing F2:
git switch main
git merge --ff-only feature
main, feature: B---F1---F2
No new commit is made. --ff-only requires this shape and exits non-zero when the histories have diverged, which makes it useful for encoding an expected history shape in a script. The default --ff fast-forwards when possible; --no-ff creates a merge commit even when a fast-forward is possible.
When both sides contain commits after their common ancestor:
F1---F2 feature
/ \
...---B---M1---M2---R main
R is a new merge commit with two parents: the previous current-branch tip M2 and the merged tip F2. It joins the histories and records the merged snapshot.
git switch main
git merge feature
git show --no-patch --format=raw HEAD
To inspect a merge result before creating its commit:
git merge --no-ff --no-commit feature
git diff --cached
git status --short
A fast-forward has no merge commit, so --no-commit alone cannot pause before it. --no-ff --no-commit creates an inspectable stopping point. Finish with git commit; abandon this merge with git merge --abort.
Before beginning, make the index agree with HEAD and preserve uncommitted work. The Git documentation warns that starting a merge with complex uncommitted changes can prevent git merge --abort from fully reconstructing the earlier state.
A content conflict means Git cannot mechanically choose a result for one overlapping text area. It commonly occurs when both sides modify the same line or overlapping hunks. Conflict markers require a human to decide the final content, and the result still needs tests.
A semantic conflict can have no conflict markers. The branches edit different files or code areas, Git merges the text successfully, yet the combined interfaces, configuration, permissions, or behavior no longer meet their agreement. A clean merge proves that Git constructed a file snapshot. It does not prove that the snapshot builds, passes tests, or satisfies a product rule.
This chapter’s lab provides a reproducible counterexample:
semantic-server changes server protocol and port to https / 8443
semantic-client changes the client address to http://service:8443
semantic-result cleanly merges the two different-file changes
The lab reads files from the real Git tree of semantic-result and runs a standard-library Python check. It expects https://service:8443, receives http://service:8443, and exits with status 1. That is a real failed behavior check, not a prose-only conflict.
The lab creates an isolated temporary repository. It disables system and global Git configuration, sets a temporary identity, disables signing, and isolates hooks. It does not modify this project:
cd labs/git-mental-model/06-merge
lab_path=$(bash setup.sh)
bash verify.sh "$lab_path"
The key assertions in verify.sh are:
ff-target names the same commit as fast-forward; that commit has one parent
merge-result has two parents
semantic-result has two parents; its behavior check exits with status 1
main is at UU conflict.txt; the index has stages 1, 2, and 3
To preserve the lab state and inspect it:
cd "$lab_path"
git log --graph --oneline --all --decorate
git show --no-patch --format=raw merge-result
git status --short
git merge-base HEAD MERGE_HEAD
git ls-files --unmerged -- conflict.txt
git show :1:conflict.txt
git show :2:conflict.txt
git show :3:conflict.txt
When finished, remove only the temporary repository marked for this lab:
bash cleanup.sh "$lab_path"
Create, assert, and clean up in one run:
bash test.sh
See expected.txt for the full expected state.
Start with:
main: B
feature: B---F1---F2
main point after git merge --ff-only feature, and how many parents does the result have?git merge --no-ff feature?main first receives M1, then runs git merge --ff-only feature?Answers:
main points at the existing F2; F2 keeps its original one parent.B and F2.--ff-only exits non-zero because the histories diverged. A regular merge would attempt a two-parent result commit.An agent reports, “The merge succeeded and there are no conflicts, so it can ship.” Which evidence set supports an approval decision?
A. Only git status has no UU paths
B. Only a merge commit exists
C. Inspect the common ancestor and result diff, confirm the index has no unmerged entries, then run affected build, test, and interface-contract checks
Choose C. A only rules out unresolved text conflicts, and B only shows that the object graph is connected. Behavioral correctness needs validation for the affected paths. If the agent cannot identify the contracts it changed, narrow the diff and the test scope before deciding whether to approve it.
The common ancestor comes from commit-parent relationships, not branch names or creation times. git merge-base calculates reachability in the object graph.
A fast-forward only moves the current ref to an existing commit. git rev-list --parents -n 1 HEAD shows that the target commit’s parent count did not increase because of the fast-forward.
Conflict markers answer how text can be combined. Builds, tests, migration checks, interface contracts, and human business judgment validate the combined meaning.
In a regular merge, ours is the current HEAD and theirs is MERGE_HEAD. Those are operation-context names, not business priority. Read the active operation and the three stage contents before using --ours or --theirs.