X Xerobit

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,...

Mian Ali Khalid · · 5 min read
Use the tool
Text Diff
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.
Open Text Diff →

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 posts

Related tool

Text Diff

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.