Resolving Merge Conflicts Like a Pro
Table of Contents
- Introduction
- Why Merge Conflicts Happen
- Anatomy of a Conflict
- Resolving Conflicts — The Basics
- Tools That Make It Easier
- Handling Complex Conflicts
- Conflicts During Rebase
- Preventing Conflicts Before They Happen
- Recovering From Resolution Mistakes
- Conclusion
Introduction
Merge conflicts are one of the most dreaded parts of working with Git. That moment when your terminal spits out CONFLICT (content): Merge conflict in src/app.ts can make even experienced engineers tense up.
But here's the thing — conflicts aren't a sign that something went wrong. They're a natural consequence of multiple people working on the same codebase. Git handles the vast majority of merges automatically. It only stops and asks for help when two branches have modified the same lines in ways it can't reconcile on its own.
The difference between a junior developer and a senior one isn't that seniors avoid conflicts. It's that seniors resolve them quickly, correctly, and without panic. This guide will get you there.
Why Merge Conflicts Happen
Git tracks changes line by line. When you merge two branches, Git compares three things: the common ancestor commit, the changes on your branch, and the changes on the incoming branch. If both branches modified different files — or even different lines in the same file — Git merges them automatically without any issue.
Conflicts arise in specific situations:
Same lines edited on both branches. This is the most common case. You changed line 42 of config.ts, and your teammate also changed line 42 in a different way. Git can't pick a winner, so it flags the conflict.
A file was edited on one branch and deleted on another. Git doesn't know if the deletion should take precedence or if the edits should be kept.
Both branches added a file with the same name. Two different files at the same path — Git needs you to decide which one survives (or how to combine them).
Rename conflicts. One branch renamed a file while another edited it. Git usually handles this, but complex renames can confuse it.
Anatomy of a Conflict
When a conflict occurs, Git modifies the affected file by inserting conflict markers. Understanding these markers is the first step to resolving conflicts confidently.
function getTimeout() {
<<<<<<< HEAD
const timeout = 3000;
const retries = 3;
=======
const timeout = 5000;
const retries = 5;
>>>>>>> feature/improved-retry
}
Here's what each section means:
<<<<<<< HEAD — marks the beginning of your current branch's version (the branch you're merging into).
======= — the divider between the two versions.
>>>>>>> feature/improved-retry — marks the end of the incoming branch's version (the branch you're merging from).
Your job: decide what the final code should look like, remove all three marker lines, and stage the result.
Resolving Conflicts — The Basics
Step 1: Identify Conflicted Files
# See which files have conflicts
git status
# Output will show something like:
# both modified: src/config.ts
# both modified: src/utils/retry.ts
Step 2: Open and Edit Each File
Open the conflicted file, find the conflict markers, and decide on the correct resolution. You have four options:
Keep your version: Delete the incoming changes and the markers.
function getTimeout() {
const timeout = 3000;
const retries = 3;
}
Keep their version: Delete your changes and the markers.
function getTimeout() {
const timeout = 5000;
const retries = 5;
}
Keep both: Combine the changes logically.
function getTimeout(env) {
const timeout = env === 'production' ? 5000 : 3000;
const retries = env === 'production' ? 5 : 3;
}
Write something entirely new: Sometimes neither version is correct in the merged context, and you need fresh logic.
Step 3: Stage and Commit
# Stage the resolved files
git add src/config.ts src/utils/retry.ts
# Commit the merge (Git pre-fills the message)
git commit
That's it. The conflict is resolved. But the real skill is doing this efficiently across multiple files, with confidence that you didn't break anything.
Tools That Make It Easier
Editing conflict markers in a plain text editor works, but dedicated tools make the process faster and less error-prone — especially with large or complex conflicts.
VS Code
VS Code detects conflict markers automatically and displays inline buttons: Accept Current Change, Accept Incoming Change, Accept Both Changes, and Compare Changes. For most conflicts, this is all you need.
Git Mergetool
# Launch the configured merge tool
git mergetool
# Set your preferred tool globally
git config --global merge.tool vscode
# VS Code as mergetool
git config --global mergetool.vscode.cmd 'code --wait --merge $REMOTE $LOCAL $BASE $MERGED'
Popular Dedicated Tools
kdiff3 — three-way merge with automatic resolution of non-overlapping changes. Great for complex conflicts.
Beyond Compare — commercial tool with excellent visual diffing. Widely used in enterprise environments.
IntelliJ / WebStorm — built-in three-panel merge tool that's arguably the best IDE-integrated conflict resolver available.
Meld — free, open-source, visual diff and merge tool. Lightweight and effective for most use cases.
Quick Resolution Shortcuts
# Accept all changes from your branch (current)
git checkout --ours src/config.ts
# Accept all changes from the incoming branch
git checkout --theirs src/config.ts
# Then stage
git add src/config.ts
⚠️ WARNING: --ours and --theirs are blunt instruments. They discard one side entirely without inspection. Only use these when you're absolutely sure one version is correct and the other should be thrown away.
Handling Complex Conflicts
Multiple Conflicts in One File
A single file can have several conflicted sections. Search for <<<<<<< to find all of them. Resolve each one individually — don't assume fixing one fixes the rest.
# Quick way to find all remaining conflict markers
grep -rn '<<<<<<<\|=======\|>>>>>>>' src/
Delete/Modify Conflicts
When one branch deleted a file and the other modified it, Git flags it differently:
# git status shows:
# deleted by them: src/old-module.ts
# Keep the file
git add src/old-module.ts
# Or accept the deletion
git rm src/old-module.ts
Binary File Conflicts
Git can't merge binary files (images, compiled assets, PDFs). You have to pick one version:
# Keep your version
git checkout --ours assets/logo.png
git add assets/logo.png
# Keep their version
git checkout --theirs assets/logo.png
git add assets/logo.png
Large-Scale Conflicts
If a merge produces dozens of conflicts, step back and ask whether the merge strategy is right. Sometimes it's better to:
Abort the merge with git merge --abort, break the work into smaller pieces, or coordinate with the other developer to resolve conflicts together in a pairing session.
Conflicts During Rebase
Rebase conflicts behave differently from merge conflicts. Because rebase replays commits one at a time, you may need to resolve conflicts at each commit rather than once for the entire branch.
# Start a rebase
git rebase main
# Conflict occurs at commit 3 of 7
# Resolve the conflict, then:
git add .
git rebase --continue
# Repeat for each commit that conflicts
# If it gets overwhelming, bail out
git rebase --abort
💡 TIP: An important detail — during a rebase, --ours and --theirs are swapped compared to a merge. In a rebase, "ours" refers to the branch you're rebasing onto (e.g., main), and "theirs" refers to your own commits being replayed. This catches people off guard constantly.
Reducing Rebase Conflicts
Squash related commits before rebasing. Five commits that each touch the same file will create five opportunities for conflicts. If you squash them into one commit first with git rebase -i, you only resolve once.
# Squash your branch's commits first
git rebase -i HEAD~5
# Then rebase onto main
git rebase main
Preventing Conflicts Before They Happen
The best conflict is one that never occurs. While you can't eliminate conflicts entirely in a team environment, these practices dramatically reduce their frequency and severity.
Pull frequently. The longer your branch diverges from main, the higher the chance of conflicts. Fetch and rebase onto main daily if active development is happening.
Keep branches short-lived. Feature branches that live for weeks accumulate drift. Aim for branches that last two to three days. Break large features into smaller, independently mergeable pieces.
Communicate with your team. If two people are editing the same file, a quick heads-up in Slack can save 30 minutes of conflict resolution later.
Use code ownership and clear module boundaries. When different team members own different parts of the codebase, conflicts naturally decrease because people aren't touching the same files.
Avoid large-scale reformatting commits. A single commit that reformats 200 files will conflict with every active branch. If you need to do this, coordinate with the team and have everyone merge their work first.
Check for conflicts before they land. Many CI systems and platforms like GitHub can flag conflicts on PRs automatically. Don't let conflicts sit — resolve them as soon as they're detected.
Recovering From Resolution Mistakes
Resolved a conflict wrong and already committed? Don't worry. Git has you covered.
Before Committing
# Restart the merge resolution from scratch
git checkout -m src/config.ts
# This restores the conflict markers so you can try again
After Committing (Merge)
# Undo the merge commit entirely
git reset --hard HEAD~1
# Or use ORIG_HEAD
git reset --hard ORIG_HEAD
# Then start the merge over
git merge feature/improved-retry
After Committing (Rebase)
# Reflog to the rescue — find the state before the rebase
git reflog
git reset --hard HEAD@{n}
After Pushing a Bad Resolution
# If others haven't pulled yet, force push the fix
git reset --hard ORIG_HEAD
git merge feature/improved-retry # redo it correctly
git push --force-with-lease
# If others have already pulled, use revert instead
git revert -m 1 <merge-commit-hash>
💡 TIP: After resolving any non-trivial conflict, run your test suite before pushing. A conflict resolution that compiles but produces wrong behavior is worse than one that fails outright — at least a build failure gets caught immediately.
Conclusion
Merge conflicts aren't the enemy. They're Git's way of saying, "I need a human decision here." The faster you can understand what both sides intended and produce the correct combined result, the less disruptive conflicts become.
The pro-level approach boils down to a few habits: understand the conflict markers instead of blindly accepting one side, use a proper merge tool for anything beyond trivial changes, run tests after resolving to catch logic errors, and prevent conflicts proactively by keeping branches short and pulling often.
Conflict resolution is a skill that improves with practice. The more you do it, the faster your instincts get. And on the rare occasion you mess it up — git reflog is always there to bail you out.