What Are the Best Strategies for Resolving Git Merge Conflicts?
What are the best strategies for resolving Git merge conflicts?
TL;DR
- Bottom line: Merge conflicts occur when Git can't auto-reconcile divergent changes to
the same lines. The fastest path: identify conflicted files → resolve markers →
git add→git commit(merge) orgit rebase --continue(rebase). - Key tool/command:
git statusto list conflicted files;git diff --diff-filter=Uto see all unmerged hunks;git mergetoolto open a GUI resolver. - Watch out for: Never commit with conflict markers
(
<<<<<<<,=======,>>>>>>>) still in files. Rungit diff --checkto catch stray markers before committing. - Works with: Git 2.0+. The
zdiff3conflict style requires Git 2.35+. Theortmerge strategy (now default) requires Git 2.33+. As of Git 2.50,recursiveis a synonym forort.
Constraints
- Never commit with conflict markers: Always run
git diff --checkbefore committing a merge resolution. Stray<<<<<<<markers in production code cause syntax errors and silent data corruption. - ours/theirs semantics flip during rebase: During
git merge,--ours= your current branch (HEAD),--theirs= incoming. Duringgit rebase,--ours= the upstream branch you're rebasing onto,--theirs= your commits being replayed. Getting this wrong silently discards the wrong side's changes. git merge -s oursis destructive: This merge strategy creates a merge commit but completely ignores the other branch's tree. It is not the same asgit merge -X ours(a strategy option that only resolves conflicted hunks with HEAD). Confusing the two can silently discard an entire branch of work. [src1]- Binary files require full-side selection: Git cannot partially merge binary files. You
must choose
--oursor--theirsentirely. [src1] - Never force-push rebased shared branches: Rebasing rewrites commit SHAs. Force-pushing a shared branch breaks every collaborator who has based work on the original commits.
- Lock files must be regenerated: Resolve
package-lock.json,yarn.lock,pnpm-lock.yamlby accepting one side and regenerating:git checkout --theirs package-lock.json && npm install. Manual edits produce invalid checksums.
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.enabled → true
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
ours/theirsmeaning flips during rebase: Duringgit merge,ours= your current branch; duringgit rebase,ours= the upstream branch you're rebasing onto. This trips up even experienced developers. [src1, src6]- Default conflict style hides the base: Without
merge.conflictStyle zdiff3, you only see two sides. Enablingzdiff3(Git 2.35+) often makes the correct resolution immediately obvious. Git core developers use zdiff3 by default. [src4, src5] - Long-lived branches accumulate conflicts: The longer a branch diverges from
main, the more conflicts accumulate. Keeping branches short (less than a week) and rebasing daily dramatically reduces complexity. [src3] git rererenot enabled by default: This powerful feature is off by default. Enable globally withrerere.enabled true. Use--no-rerere-autoupdateif you want to review rerere's resolutions before they're staged. [src2, src4]- Backup files from mergetool: By default,
git mergetoolcreates.origbackup files. Setmergetool.keepBackup falseglobally to suppress them, and add*.origto.gitignore. [src1, src7] - Forgetting to
git addafter manual resolution: After editing a conflicted file, you mustgit addit to mark it resolved. Just saving the file is not enough — Git still considers it conflicted until staged. [src1, src6] recursivestrategy removed in Git 2.50: As of Git 2.50, therecursivemerge strategy is a synonym forort. If your CI scripts reference-s recursiveexplicitly, they still work but are redirected. New scripts should useortdirectly. [src8]
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] |
recursive → ort 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
- Never force-push a rebased shared branch without team agreement: Rebasing rewrites commit SHAs. If teammates have based work on the original commits, force-pushing will cause them major problems. Only rebase branches that are purely local or clearly marked as regularly rebased.
git merge -X oursis notgit merge -s ours:-X oursis a strategy option (resolves conflicts with HEAD while incorporating the merge).-s oursis a strategy (creates a merge commit but completely ignores the other branch's tree). The latter is rarely what you want. [src1]- Lock files always conflict: Handle
package-lock.json,yarn.lock,Pipfile.lockby accepting one side and regenerating:git checkout --theirs package-lock.json && npm install. - Binary file conflicts require full side selection: Git cannot auto-resolve binary file
conflicts. You must choose
--oursor--theirsentirely. There are no partial resolutions for binary files. [src1] - Squash merges can stale rerere cache: If your workflow squash-merges PRs, individual
commit SHAs change and
rererecache entries become stale. Evaluate whetherrererefits your merge strategy. [src2] ortstrategy differences: Theortmerge strategy (default since Git 2.33, sole backend since 2.50) handles rename detection better than the oldrecursivestrategy but may produce different conflict markers for edge cases involving directory renames. If you see unexpected conflicts after upgrading, checkgit log --diff-filter=Rfor renames. [src8]