Merge conflicts occur when Git cannot automatically reconcile differing modifications made to the exact same lines of a file across divergent branches, or when a file has been modified in one branch and deleted in another.
While modern Git employs sophisticated 3-way merge heuristics (such as the ort and recursive merge strategies), developer friction usually stems from cryptic conflict markers (<<<<<<<, =======, >>>>>>>) and a lack of context regarding what the code looked like before either branch modified it.
This guide details the anatomy of standard and diff3 conflict markers, explains how 3-way merges work under the hood, provides safe rebase conflict resolution workflows, and shares strategies to prevent merge conflicts.
1. Anatomy of Git Conflict Markers
When a conflict occurs during git merge feature-branch or git pull, Git injects conflict markers directly into your working tree files.
Standard 2-Way Conflict Markers (Default)
<<<<<<< HEAD (Current change / Target branch)
const API_URL = "https://api.v2.production.com";
const TIMEOUT_MS = 5000;
=======
const API_URL = "https://api-gateway.internal.net";
const TIMEOUT_MS = 10000;
const RETRY_COUNT = 3;
>>>>>>> feature-branch (Incoming change)
<<<<<<< HEAD: Marks the start of changes from the branch you are currently on.=======: The divider separating your current branch from the incoming branch.>>>>>>> <branch_name>: Marks the end of changes coming from the branch being merged.
The Secret Weapon: Enabling diff3 (3-Way Conflict Style)
By default, standard markers omit the common ancestor (base) version, forcing you to guess why each developer made their changes.
Enable diff3 or zdiff3 globally to show what the code looked like at the merge base:
git config --global merge.conflictstyle zdiff3
Now conflict markers include the base version:
<<<<<<< HEAD
const API_URL = "https://api.v2.production.com";
const TIMEOUT_MS = 5000;
||||||| base (Common ancestor before either branch diverged)
const API_URL = "https://api.legacy.com";
const TIMEOUT_MS = 5000;
=======
const API_URL = "https://api.legacy.com";
const TIMEOUT_MS = 10000;
const RETRY_COUNT = 3;
>>>>>>> feature-branch
With diff3, you instantly see that HEAD updated API_URL while feature-branch updated TIMEOUT_MS and added RETRY_COUNT. The resolution is obvious: keep both updates!
Tip: Paste raw conflict blocks into our DevFlow Git Conflict Resolver to visually select incoming vs current changes with clean syntax highlighting.
2. Step-by-Step Merge Conflict Resolution Workflow
# 1. Initiate merge and discover conflict
git checkout main
git merge feature-login
# Output: Auto-merging src/auth.ts
# CONFLICT (content): Merge conflict in src/auth.ts
# Automatic merge failed; fix conflicts and then commit the result.
# 2. Check which files are in the unmerged state
git status -s
# UU src/auth.ts (UU = both modified)
# 3. Open the file and manually edit to produce the desired final state,
# removing all <<<<<<<, |||||||, =======, and >>>>>>> markers.
# 4. Stage the resolved files
git add src/auth.ts
# 5. Complete the merge commit
git commit -m "Merge branch 'feature-login' into main - resolve auth config conflict"
3. Resolving Conflicts During git rebase
Rebasing rewrites commit history by replaying commits sequentially onto a new base. When conflicts occur during a rebase:
# 1. Start rebase onto updated main
git checkout feature-branch
git rebase main
# 2. When Git pauses on a conflicting commit:
git status
# 3. Resolve the conflict in the affected files and stage them:
git add src/auth.ts
# 4. Resume the rebase without creating a merge commit:
git rebase --continue
# Note: If the rebase gets hopelessly corrupted, you can abort safely:
git rebase --abort
4. Conflict Resolution Strategies & Commands Cheat Sheet
| Scenario | Command | Effect |
|---|---|---|
| Accept Entire Incoming Branch | git checkout --theirs <file> |
Overwrites file completely with incoming branch changes. |
| Accept Entire Current Branch | git checkout --ours <file> |
Keeps current branch version and discards incoming changes. |
| Abort Merge Safely | git merge --abort |
Returns working directory to pre-merge state cleanly. |
| View Differences from Base | git diff --base <file> |
Compares current working file with common ancestor. |
| Compare Line Diffs Visually | Use Text Diff Checker | Highlights precise character and line additions/deletions. |
Frequently Asked Questions
What is the difference between --ours in git merge vs git rebase?
During git merge, --ours refers to your current branch (e.g. main) and --theirs refers to the incoming branch (e.g. feature). During git rebase, this is inverted: --ours represents the upstream branch you are rebasing onto (main), and --theirs represents the commit on your feature branch currently being replayed.
How do I prevent merge conflicts before they happen?
- Keep feature branches short-lived (merge within 1-2 days).
- Regularly rebase or merge
maininto your feature branch. - Split massive refactoring PRs (e.g. linter formatting or file renames) into isolated standalone PRs.
- Establish clear code boundaries so multiple engineers are not editing the same file concurrently.