What Are the Best Strategies for Resolving Git Merge Conflicts?

Type: Software Reference Confidence: 0.95 Sources: 8 Verified: 2026-02-23 Freshness: yearly

TL;DR

Constraints

Quick Reference

# Scenario Command / Solution Notes
1 List all conflicted files git status or git diff --name-only --diff-filter=U Files marked UU (both modified) [src1]
2 Accept current branch for one file git checkout --ours <file> "Ours" = branch you're on (HEAD) [src1, src6]
3 Accept incoming branch for one file git checkout --theirs <file> "Theirs" = branch being merged in [src1, src6]
4 Accept ours for entire merge git merge -X ours <branch> Silently resolves all conflicts with HEAD version [src1, src3]
5 Accept theirs for entire merge git merge -X theirs <branch> Silently resolves all conflicts with incoming version [src1, src3]
6 Open GUI merge tool git mergetool Uses configured tool (VS Code, vimdiff, meld) [src1, src7]
7 Show common ancestor in markers git config merge.conflictStyle diff3 Adds ||||||| section with base version [src5, src6]
8 Use improved zdiff3 (recommended) git config --global merge.conflictStyle zdiff3 Zealously removes common context from conflict markers (Git 2.35+) [src4, src5]
9 Enable rerere (auto-reuse resolutions) git config --global rerere.enabled true Records and replays conflict resolutions [src2]
10 Abort a merge git merge --abort Returns to pre-merge state [src1]
11 Abort a rebase git rebase --abort Returns to pre-rebase state [src1]
12 Continue after rebase conflict git add <file> && git rebase --continue Move to next commit in rebase [src1]
13 Skip conflicted commit in rebase git rebase --skip Drops the conflicting commit entirely [src1]
14 View conflict history git log --merge --oneline Shows commits that caused the conflict [src1]
15 AI-assisted resolution (VS Code) Open conflicted file → "Resolve with Copilot" Requires GitHub Copilot subscription; uses merge base as context [src7]

Decision Tree

START — Merge conflict encountered
├── Is this a merge or a rebase?
│   ├── MERGE
│   │   ├── Abort? → git merge --abort [src1]
│   │   ├── Accept one side for all files?
│   │   │   ├── Keep HEAD → git merge -X ours <branch> [src1, src3]
│   │   │   └── Keep incoming → git merge -X theirs <branch> [src1, src3]
│   │   └── Resolve file by file:
│   │       ├── One side wins → git checkout --ours/--theirs <file> [src1, src6]
│   │       ├── Need to combine → edit manually, remove markers [src6]
│   │       ├── Complex conflict → git mergetool (GUI) [src1, src7]
│   │       └── AI-assisted → VS Code "Resolve with Copilot" (experimental) [src7]
│   └── REBASE (conflicts appear per commit replayed)
│       ├── Abort? → git rebase --abort [src1]
│       ├── Resolve → git add <file> → git rebase --continue [src1]
│       └── Skip commit? → git rebase --skip [src1]
├── After resolving each file:
│   ├── git add <resolved-file>
│   ├── git diff --check  (verify no stray markers)
│   └── git commit (merge) or git rebase --continue (rebase)
├── Lock file conflict?
│   └── Accept one side → regenerate → git checkout --theirs package-lock.json && npm install [src6]
└── Recurring conflicts? → git config --global rerere.enabled true [src2]

Step-by-Step Guide

1. Understand conflict markers

When a conflict occurs, Git inserts markers into the file. [src1, src5, src6]

<<<<<<< HEAD (current branch)
function greet(name) {
  return `Hello, ${name}!`;
}
||||||| base (common ancestor — only with diff3/zdiff3 style)
function greet(name) {
  return 'Hello, ' + name + '!';
}
======= (separator)
function greet(name, greeting = 'Hello') {
  return `${greeting}, ${name}!`;
}
>>>>>>> feature/greeting-improvements (incoming branch)

Enable zdiff3 style to see the common ancestor (strongly recommended — used by Git core developers): [src4, src5]

# Git 2.35+ — zealously removes common context from markers
git config --global merge.conflictStyle zdiff3
# Older Git — still better than default
git config --global merge.conflictStyle diff3

Verify: git config merge.conflictStyle → expected: zdiff3

2. Find and triage all conflicts

git status                              # UU = unmerged
git diff --name-only --diff-filter=U   # filenames only
git diff                                # all conflict hunks
git log --merge --oneline --left-right  # contributing commits

Verify: git diff --name-only --diff-filter=U → lists all files needing resolution

3. Resolve manually

Edit each conflicted file, remove all conflict markers, and keep the correct code. [src1, src6]

# Edit file → remove all <<<<<<<, =======, >>>>>>> markers
git add path/to/resolved-file.js
git diff --check          # verify no stray markers
git commit                # finalise the merge

Verify: git diff --check → empty output means no stray markers

4. Accept one side entirely

When one side's changes are definitively correct: [src1, src3, src6]

git checkout --ours   src/config.json && git add src/config.json
git checkout --theirs src/api/client.js && git add src/api/client.js

# In rebase: ours/theirs are SWAPPED
# --ours = upstream branch; --theirs = your feature commit being replayed

Verify: git status → file should show as staged (green), no longer UU

5. Use a merge tool for complex conflicts

A visual three-way merge tool is much faster for multi-hunk conflicts. [src1, src7]

# Configure VS Code as merge tool (includes 3-way merge editor + AI assist)
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'
git config --global mergetool.keepBackup false

# Launch
git mergetool                        # all conflicted files
git mergetool path/to/file.js        # specific file

Verify: git status → all conflicted files should be resolved

6. Enable rerere — avoid resolving the same conflict twice

rerere records conflict resolutions and automatically replays them. [src2, src6]

git config --global rerere.enabled true
git config --global rerere.autoupdate true  # auto-stage rerere fixes

git rerere status          # show tracked conflicts
git rerere diff            # show recorded resolution
git rerere forget file.js  # discard a bad resolution

Verify: git config rerere.enabledtrue

Code Examples

Bash: automated conflict detection and summary

Full script: script-automated-conflict-detection-and-summary.sh (32 lines)

#!/bin/bash
echo "=== Git Conflict Summary ==="
echo "Branch: $(git branch --show-current)"

CONFLICT_FILES=$(git diff --name-only --diff-filter=U 2>/dev/null)
CONFLICT_COUNT=$(echo "$CONFLICT_FILES" | grep -c . 2>/dev/null || echo 0)

[ "$CONFLICT_COUNT" -eq 0 ] && echo "No conflicts" && exit 0

echo "$CONFLICT_COUNT conflicted file(s):"
while IFS= read -r file; do
    [ -z "$file" ] && continue
    HUNKS=$(grep -c "^<<<<<<< " "$file" 2>/dev/null || echo 0)
    echo "  $file ($HUNKS hunk(s))"
    grep -n "^<<<<<<< \|^>>>>>>> \|^=======" "$file" 2>/dev/null | head -4 | sed 's/^/     /'
done <<< "$CONFLICT_FILES"

echo ""
echo "Resolution: edit files → git add <file> → git commit"
echo "GUI:        git mergetool"
echo "Abort:      git merge --abort | git rebase --abort"

Python: pre-commit hook — detect conflict markers

Full script: python-pre-commit-hook-detect-conflict-markers.py (53 lines)

#!/usr/bin/env python3
"""
Pre-commit hook — blocks commits containing unresolved conflict markers.
Install: cp this_file .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
"""
import subprocess, sys, re

MARKERS = re.compile(r'^(<{7}|={7}|>{7}|\|{7})', re.MULTILINE)

def get_staged_files():
    r = subprocess.run(['git','diff','--cached','--name-only','--diff-filter=ACMR'],
                       capture_output=True, text=True)
    return [f.strip() for f in r.stdout.splitlines() if f.strip()]

def check_file(path):
    r = subprocess.run(['git','show',f':{path}'], capture_output=True, text=True)
    if r.returncode != 0: return []
    return [(i+1, l.rstrip()) for i,l in enumerate(r.stdout.splitlines())
            if MARKERS.match(l)]

found = False
for f in get_staged_files():
    hits = check_file(f)
    if hits:
        found = True
        print(f"Conflict markers in {f}:")
        for lineno, line in hits:
            print(f"   Line {lineno}: {line}")

if found:
    print("\nResolve conflicts first: git mergetool | git diff --check")
    sys.exit(1)
sys.exit(0)

Bash: clean feature branch rebase workflow

Full script: git-workflow-feature-branch-with-clean-rebase.sh (40 lines)

#!/bin/bash
FEATURE="${1:-$(git branch --show-current)}"
TARGET="${2:-main}"

echo "=== Rebasing $FEATURE onto $TARGET ==="
git fetch origin "$TARGET"
git checkout "$FEATURE"
git rebase "origin/$TARGET"

if [ $? -ne 0 ]; then
    echo "Conflicts detected. For each conflicted file:"
    echo "  1. Resolve in editor (remove <<<<<<< markers)"
    echo "  2. git add <resolved-file>"
    echo "  3. git rebase --continue"
    echo ""
    echo "Cancel: git rebase --abort"
    echo "Skip commit: git rebase --skip"
    echo "Tip: git config --global rerere.enabled true (auto-replay resolutions)"
    exit 1
fi

echo "Rebase complete"
git checkout "$TARGET"
git merge --ff-only "$FEATURE"
echo "Fast-forward merge complete — push: git push origin $TARGET"

Anti-Patterns

Wrong: Resolving rebase conflicts with --ours when you mean your feature branch

# BAD — during rebase, --ours = UPSTREAM branch (not your feature!) [src1, src6]
git rebase main
# conflict appears...
git checkout --ours conflicted-file.js   # keeps main's version silently!
git add conflicted-file.js
git rebase --continue
# Your feature changes are lost with no warning!

Correct: During rebase, --theirs is your feature branch

# GOOD — understand the ours/theirs flip in rebase [src1, src6]
# Rebasing feature onto main:
#   --ours   = main (upstream)
#   --theirs = your feature commit being replayed
git checkout --theirs conflicted-file.js
git add conflicted-file.js
git rebase --continue

# When in doubt, inspect explicitly:
git show :2:conflicted-file.js  # "ours" (main)
git show :3:conflicted-file.js  # "theirs" (feature commit)

Wrong: Committing without verifying conflict markers are gone

# BAD — committing with stray markers [src1, src6]
git add src/utils.js
git commit -m "resolve conflict"
# <<<<<<< HEAD is now in production code!

Correct: Always run git diff --check before committing

# GOOD — verify no markers remain [src1, src6]
git diff --check
# Empty output means safe to commit
# Use pre-commit hook (see code example above) to automate this

Wrong: Using git merge -X theirs when only some files should accept theirs

# BAD — silently overwrites ALL conflicts with one side [src1, src3]
git merge -X theirs feature/big-refactor
# No review — even files you wanted to keep are overwritten

Correct: Per-file intentional resolution

# GOOD — make intentional per-file decisions [src1, src3]
git merge feature/big-refactor   # let it conflict
git checkout --theirs src/api/client.js    # their refactor is better
git checkout --ours   src/config/env.js    # keep our environment config
# Manually resolve src/utils/helpers.js    # both sides have useful changes
git add src/api/client.js src/config/env.js src/utils/helpers.js
git diff --check
git commit

Wrong: Using default conflict style without common ancestor context

# BAD — default conflict style shows only two sides [src4, src5]
# Without diff3/zdiff3, you can't see what the original code looked like:
# <<<<<<< HEAD
# const timeout = 5000;
# =======
# const timeout = 10000;
# >>>>>>> feature/performance
# Was it 3000 before? 5000? You don't know — forced to guess.

Correct: Configure zdiff3 globally to see the base version

# GOOD — zdiff3 shows the common ancestor with minimized noise [src4, src5]
git config --global merge.conflictStyle zdiff3

# Now conflicts show the base version:
# <<<<<<< HEAD
# const timeout = 5000;
# ||||||| base
# const timeout = 3000;
# =======
# const timeout = 10000;
# >>>>>>> feature/performance
# Now you can see both sides changed from 3000 — make an informed decision.

Common Pitfalls

Diagnostic Commands

Full script: diagnostic-commands.sh (31 lines)

# Find conflicts
git status
git diff --name-only --diff-filter=U
git diff
git log --merge --oneline --left-right

# Inspect both sides of a conflict
git show :1:file.js   # common ancestor
git show :2:file.js   # ours (HEAD)
git show :3:file.js   # theirs (incoming)

# Quick resolution
git checkout --ours file.js
git checkout --theirs file.js
git add file.js

# After resolving
git diff --check    # verify no stray markers
git status          # confirm no more UU files

# Abort
git merge --abort
git rebase --abort
git cherry-pick --abort

# rerere
git rerere status
git rerere diff
git rerere forget file.js
ls .git/rr-cache/

# Global configuration (recommended)
git config --global merge.conflictStyle zdiff3
git config --global rerere.enabled true
git config --global rerere.autoupdate true
git config --global merge.tool vscode
git config --global mergetool.keepBackup false

Version History & Compatibility

Feature Available Since Notes
git rerere Git 1.5.4 Enable with rerere.enabled true [src2]
merge.conflictStyle diff3 Git 1.6.1 Shows common ancestor in conflict markers [src5]
merge.conflictStyle zdiff3 Git 2.35 (Jan 2022) Improved diff3 — zealously removes common context from markers [src4, src5]
git checkout --ours/--theirs Git 1.6.1 Per-file resolution [src1]
git merge -X ours/theirs Git 1.7.0 Whole-merge strategy option [src1]
git mergetool Git 1.5.1 Launch configured merge tool [src1]
git merge --abort Git 1.7.4 Before: git reset --merge [src1]
git rebase --abort/--continue Git 1.7.8 Safely cancel or continue a rebase [src1]
ort merge strategy (default) Git 2.33 (Aug 2021) Faster replacement for recursive; better rename detection [src8]
recursiveort redirect Git 2.50 (2025) recursive is now a synonym for ort [src8]
VS Code AI-assisted merge VS Code 1.105 (2025) Requires GitHub Copilot subscription [src7]

When to Use / When Not to Use

Strategy When Why
git merge Shared/public branches Preserves full history; safe for collaborators [src3]
git rebase Private feature branches Linear history; cleaner PR diffs [src3]
git merge -X ours/theirs Auto-generated files (lock files, changelogs) One side is definitively correct [src1, src3]
rerere Frequent rebases; long-lived branches Avoid re-resolving the same conflict [src2]
zdiff3 style Always More context = better resolution decisions; recommended by Git core devs [src4, src5]
git mergetool Complex multi-hunk conflicts Visual three-way merge is faster [src7]
AI-assisted (VS Code Copilot) Conflicts with clear intent on both sides AI considers merge base + both branches; experimental, review output carefully [src7]

Important Caveats

Related Units