Understanding Merge Conflicts — How 3-Way Diff Works
Merge conflicts occur when two branches edit the same lines differently. Learn how 3-way diff detects conflicts, what conflict markers mean, strategies for resolving conflicts,...
Merge conflicts are the natural result of parallel development. Understanding how 3-way merge works — and what the conflict markers mean — makes resolution much faster.
Use the text compare tool to compare text versions and understand what changed.
How 3-way merge works
A 2-way diff compares A and B directly. A 3-way merge uses a common ancestor (base) to determine who changed what:
Base
/ \
Branch A Branch B
3-way merge compares:
- Base → Branch A (what A changed)
- Base → Branch B (what B changed)
If A and B changed different lines: auto-merge succeeds. If A and B both changed the same line differently: conflict.
Base:
x = 10
Branch A changes line to:
x = 20
Branch B changes line to:
x = 30
Both changed the same line differently → CONFLICT
Reading conflict markers
<<<<<<< HEAD
x = 20 ← your branch's version
=======
x = 30 ← incoming branch's version
>>>>>>> feature/update-x
The <<<<<<< HEAD to ======= block is your current branch. The ======= to >>>>>>> branch-name block is theirs (the branch being merged in).
Three-way markers (with diff3 style enabled):
git config merge.conflictstyle diff3
<<<<<<< HEAD
x = 20 ← ours
||||||| base
x = 10 ← original (base)
=======
x = 30 ← theirs
>>>>>>> feature/update-x
The middle ||||||| base block shows the original value — extremely helpful context.
Resolving conflicts
# 1. See which files have conflicts:
git status
# both modified: src/config.js
# 2. Open file, find <<<< markers, edit to desired state
# 3. After editing, mark as resolved:
git add src/config.js
# 4. Complete the merge:
git commit
Resolution choices:
- Keep ours: Delete the theirs block + markers
- Keep theirs: Delete the ours block + markers
- Keep both: Combine both changes
- Rewrite: Write something entirely new
Using git mergetool
# Open visual merge tool (VS Code, vimdiff, etc.):
git mergetool
# Configure VS Code as mergetool:
git config merge.tool vscode
git config mergetool.vscode.cmd 'code --wait $MERGED'
# Configure IntelliJ:
git config merge.tool intellij
VS Code’s merge editor shows three panes: ours (top-left), theirs (top-right), base (below), result (bottom).
Common strategies
Take all ours or all theirs
# Accept all incoming (theirs) for a specific file:
git checkout --theirs src/package-lock.json
git add src/package-lock.json
# Accept all ours:
git checkout --ours src/package-lock.json
# Accept all theirs for the entire merge:
git merge -X theirs feature-branch
# Accept all ours:
git merge -X ours feature-branch
Ours merge strategy (whole branch)
# Keep our branch entirely, discard theirs:
git merge -s ours feature-branch
# Creates a merge commit but all content is from HEAD
Conflict prevention strategies
# Rebase often to stay near main:
git fetch origin
git rebase origin/main
# Check for potential conflicts before merging:
git merge --no-commit --no-ff feature-branch
git diff --cached # See what would change
git merge --abort # Abort if you don't like it
# Merge more frequently (avoid long-lived branches)
Python: detect conflicts in text
def find_conflicts(text: str) -> list[dict]:
"""Find all conflict regions in text with markers."""
conflicts = []
lines = text.splitlines()
state = 'clean'
current = {'ours': [], 'theirs': [], 'base': [], 'start': 0}
for i, line in enumerate(lines):
if line.startswith('<<<<<<<'):
state = 'ours'
current = {'ours': [], 'theirs': [], 'base': [], 'start': i}
elif line.startswith('|||||||') and state == 'ours':
state = 'base'
elif line == '=======' and state in ('ours', 'base'):
state = 'theirs'
elif line.startswith('>>>>>>>') and state == 'theirs':
current['end'] = i
conflicts.append(current)
state = 'clean'
elif state == 'ours':
current['ours'].append(line)
elif state == 'base':
current['base'].append(line)
elif state == 'theirs':
current['theirs'].append(line)
return conflicts
# Check if file has unresolved conflicts:
def has_conflicts(filepath: str) -> bool:
with open(filepath) as f:
return '<<<<<<<' in f.read()
Related tools
- Text Diff Tool — compare two text versions
- Git Diff Guide — using git diff
- Text Diff Algorithm — how diff algorithms work
Related posts
- How Diff Tools Work: Myers Algorithm, Unified Format, and Merge Conflicts — A technical walkthrough of how diff works: the Myers algorithm, the three output…
- Diff Algorithm Explained — How Text Comparison Tools Work — Text diff tools use the LCS (Longest Common Subsequence) or Myers diff algorithm…
- File Diff Tools — Best Tools for Comparing Files and Directories — File diff tools show what changed between files or folders. Here's a comparison …
- Git Diff Guide — Reading and Using git diff Commands — git diff shows what changed between commits, branches, or the working tree. Here…
- Patch File Format — How .patch Files Work and How to Apply Them — Patch files contain diff output you can apply to source files. Here's how the un…
Related tool
Compare two text blocks line-by-line or word-by-word. Unified and split view. Shows added, removed, and changed segments with full color coding.
Written by Mian Ali Khalid. Part of the Dev Productivity pillar.