Reading diff3 and zdiff3 conflict markers Jump to heading
Git’s default conflict markers show two versions of a block — yours and theirs — and leave you to work out what each side changed. That is often impossible from two snapshots alone. If one side reads timeout = 45 and the other timeout = 30, you cannot tell whether one person raised it from 30, the other lowered it from 45, or both changed it from 60. The ancestor version answers that instantly, and Git can include it in the markers. With merge.conflictStyle set to diff3 or the newer zdiff3, every conflict shows what the code looked like before either side touched it. This page explains how to read the extra section and why it changes how you resolve conflicts, within 3-way merge fundamentals.
When to use this approach Jump to heading
- You resolve conflicts regularly and sometimes cannot tell which side changed what.
- Conflicts involve values or small edits where both sides look plausible.
- You review other people’s conflict resolutions and want to see what the original was.
- Your team uses rebase heavily, where the meaning of “ours” and “theirs” flips — the ancestor view removes the need to remember which is which; see ours and theirs during merge vs rebase.
Step 1 — Turn on zdiff3 Jump to heading
zdiff3 (Git 2.35 and newer) is diff3 with one improvement: lines that are identical at the start or end of both sides are moved outside the conflict, so the markers enclose only what actually differs. Use diff3 on older Git.
git config --global merge.conflictStyle zdiff3 # Git 2.35+
# git config --global merge.conflictStyle diff3 # older Git
git config --get merge.conflictStyle The setting affects merge, rebase, cherry-pick, revert and stash application — every operation that can produce conflict markers.
Step 2 — Recognise the three sections Jump to heading
With an ancestor-style setting, a conflict has three sections instead of two. The middle one, introduced by |||||||, is the merge base: the version both sides started from.
<<<<<<< HEAD
retry_limit = 5
timeout = 45
||||||| merge base
retry_limit = 3
timeout = 30
=======
retry_limit = 3
timeout = 60
>>>>>>> feature/slow-backends Read it as two diffs against the middle. Ours changed retry_limit from 3 to 5 and timeout from 30 to 45. Theirs left retry_limit alone and changed timeout from 30 to 60. Now the conflict is clear: both sides raised the timeout, by different amounts, and only ours touched the retry limit.
# Verification: produce a real conflict and look at the markers
git merge feature/slow-backends || sed -n '/<<<<<<</,/>>>>>>>/p' config/backends.ini Step 3 — Resolve by combining intents, not picking a side Jump to heading
With two-way markers, the temptation is to pick one side. With the base visible, you can see each side’s intent and usually combine them. In the example, keep ours’ retry limit (theirs did not care about it) and decide the timeout deliberately — both wanted it higher, so the higher value, or a conversation, is the right answer.
retry_limit = 5
timeout = 60 A useful habit: for each line in the conflict, ask “did ours change it, did theirs, or both?” If only one side changed a line, take that side’s version. Only lines changed by both sides need judgement.
Step 4 — Use zdiff3 to shrink noisy conflicts Jump to heading
When both sides change the same region and happen to make some identical edits, diff3 includes the identical lines inside the conflict, making it longer than it needs to be. zdiff3 moves common leading and trailing lines out.
# diff3: identical first and last lines are inside the conflict
<<<<<<< HEAD
import logging
import requests
from app import config
||||||| merge base
import requests
=======
import logging
import httpx
from app import config
>>>>>>> feature/httpx
# zdiff3: only the line that truly differs remains in conflict
import logging
<<<<<<< HEAD
import requests
||||||| merge base
import requests
=======
import httpx
>>>>>>> feature/httpx
from app import config Now it is obvious: ours did not change the HTTP client import at all (it matches the base), so theirs’ change to httpx should win.
Step 5 — Recover the base when markers are not enough Jump to heading
Sometimes the conflict is in a file too large or too restructured to read in markers. All three versions are available as index stages while the conflict is unresolved, so you can open them side by side or diff them directly.
git show :1:src/client.py > /tmp/base.py # stage 1: merge base
git show :2:src/client.py > /tmp/ours.py # stage 2: ours
git show :3:src/client.py > /tmp/theirs.py # stage 3: theirs
diff -u /tmp/base.py /tmp/theirs.py # what did they change? If you edited the markers into a mess and want to start the file again, git checkout --conflict=zdiff3 -- path regenerates the conflict markers from the stages without touching other files.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Does zdiff3 change how Git merges, or only how it displays conflicts? Jump to heading
Only the display. The merge result is identical; zdiff3 changes which lines appear inside the markers. It is safe to set globally.
Why does the base section sometimes look unrelated to either side? Jump to heading
When there are several merge bases — after criss-cross merges — Git first merges the bases into a virtual ancestor, and the base section shows that synthetic version. The situation is explained in resolving criss-cross merges.
Do merge tools show the base too? Jump to heading
Most three-way merge tools show it as a separate pane. Configuring one is covered in configuring git mergetool for three-way resolution.
Related Jump to heading
- 3-Way Merge Fundamentals — the parent topic.
- Finding the Merge Base and Why It Matters — where the middle section comes from.
- Resolving Conflicts When Popping a Stash — the same markers in a stash context.
- Reviewing Conflict Resolutions with Remerge-Diff — checking how someone else resolved one.