How to Fix Git Merge Conflicts: Step-by-Step Guide With Examples
How to Fix Git Merge Conflicts: Step-by-Step Guide With Examples
If you've spent any time working with Git in a team environment, you've almost certainly run into a merge conflict. That jarring moment when Git stops and tells you it can't automatically combine two sets of changes is one of the most common stumbling blocks for developers at every level. The good news: merge conflicts are completely solvable, and once you understand what's actually happening, resolving them becomes a routine part of your workflow rather than a source of dread.
This guide walks through everything you need to know — what causes conflicts, how to read the conflict markers Git inserts into your files, and the exact steps to resolve them cleanly.
What Is a Git Merge Conflict?
A merge conflict occurs when Git tries to combine changes from two different branches and finds that both branches have modified the same part of the same file in incompatible ways. Git is smart enough to automatically merge changes that don't overlap — if you edited line 10 and your colleague edited line 50, Git handles that silently. But when both of you changed line 10 differently, Git has no way to know which version is correct. It stops the merge and asks you to decide.
Conflicts can also occur when:
- One branch deletes a file that another branch modified
- Both branches rename the same file to different names
- Changes in one branch conflict with restructured code in another
Setting Up: Before You Merge
A few habits prevent conflicts from becoming bigger problems than they need to be.
Keep branches short-lived. The longer a feature branch lives without being merged, the more it diverges from the main branch. Conflicts compound over time. Merge or rebase against your main branch frequently.
Pull before you push. Always fetch and integrate the latest changes before starting a merge:
git fetch origin
git merge origin/main
Communicate with your team. If you know a colleague is working on the same files, coordinate who changes what. Simple communication prevents many conflicts before they start.
Step 1: Attempt the Merge
Start the merge as you normally would:
git checkout main
git merge feature/user-authentication
If there are no conflicts, Git completes the merge automatically and you're done. If conflicts exist, you'll see output like this:
Auto-merging src/auth/login.js
CONFLICT (content): Merge conflict in src/auth/login.js
Auto-merging src/utils/helpers.js
Automatic merge failed; fix conflicts and then commit the result.
Git is telling you exactly which files have conflicts. The merge is paused — nothing has been committed yet.
Step 2: Identify All Conflicted Files
Run git status to get a clear picture of what needs attention:
git status
Output:
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: src/auth/login.js
both modified: src/utils/helpers.js
no changes added to commit (use "git add" to track changes or "git rm" to delete them)
The "Unmerged paths" section lists every file with conflicts. Work through each one.
Step 3: Understand the Conflict Markers
Open one of the conflicted files. Git has annotated the conflicting sections with special markers:
function loginUser(username, password) {
<<<<<<< HEAD
const hashedPassword = bcrypt.hashSync(password, 10);
return db.users.findOne({ username, password: hashedPassword });
=======
const hashedPassword = bcrypt.hash(password, 10);
return db.users.findOne({ username }).then(user => {
return bcrypt.compare(password, user.password);
});
>>>>>>> feature/user-authentication
}
Here's what each marker means:
<<<<<<< HEAD— marks the start of your current branch's version (the branch you merged into)=======— the dividing line between the two versions>>>>>>> feature/user-authentication— marks the end of the incoming branch's version
Everything between <<<<<<< HEAD and ======= is what your branch had. Everything between ======= and >>>>>>> is what the incoming branch introduced.
Your job is to edit this section into the correct final state, removing all three markers in the process.
Step 4: Resolve the Conflict
You have three choices for each conflict:
Option A: Keep your version (HEAD)
Delete everything from ======= through >>>>>>> feature/... and remove the <<<<<<< HEAD marker.
Option B: Keep the incoming version
Delete everything from <<<<<<< HEAD through ======= and remove the >>>>>>> feature/... marker.
Option C: Write a combined version
This is the most common real-world scenario. You look at both versions, understand the intent of each change, and write code that incorporates both correctly.
For the example above, the incoming branch switched from a synchronous bcrypt call to the asynchronous version and added proper comparison logic — which is actually the better approach. But you also want to keep the structure clean. A resolved version might look like:
function loginUser(username, password) {
return db.users.findOne({ username }).then(user => {
if (!user) return null;
return bcrypt.compare(password, user.password).then(match => {
return match ? user : null;
});
});
}
The conflict markers are gone. The function is clean and correct. That's a resolved conflict.
Step 5: Mark the File as Resolved
Once you've edited the file and removed all conflict markers, stage it:
git add src/auth/login.js
Repeat this for every conflicted file. You can verify you've caught all conflicts by running git status again — all files should move from "Unmerged paths" to "Changes to be committed."
A quick way to check that you haven't accidentally left any conflict markers in your files:
git diff --check
If this command returns nothing, you're clean. If it flags lines, you have leftover markers to find and remove.
Step 6: Complete the Merge
Once all conflicts are resolved and staged, commit the merge:
git commit
Git pre-populates a commit message describing the merge. You can accept it or customize it. The merge is now complete.
Using a Merge Tool for Visual Resolution
For complex conflicts, a visual diff tool makes resolution much easier. Git supports several out of the box:
git mergetool
This opens your configured merge tool for each conflicted file in sequence. Popular options include:
- VS Code — excellent built-in merge editor with "Accept Current", "Accept Incoming", and "Accept Both" buttons
- IntelliJ / WebStorm — three-panel diff view with clear controls
- vimdiff — powerful for terminal-only environments
- Meld — a standalone GUI tool that works well on Linux and macOS
To configure VS Code as your merge tool:
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'
VS Code's merge editor is particularly useful because it shows your version, the incoming version, and a live preview of the result in three panes simultaneously.
Aborting a Merge
If you've started resolving conflicts and realize you need to start over — or you want to abandon the merge entirely — you can abort:
git merge --abort
This returns your branch to exactly the state it was in before you ran git merge. Nothing is lost. You can then coordinate with your team, update your understanding of the codebase, and try again when ready.
Preventing Conflicts With Rebase
Some teams use git rebase instead of git merge to integrate changes. Rebasing rewrites your branch's commits on top of the target branch rather than creating a merge commit:
git checkout feature/user-authentication
git rebase main
Conflicts are resolved one commit at a time during a rebase, which keeps the history linear and often makes conflicts easier to understand in context. After resolving each conflict:
git add <resolved-file>
git rebase --continue
If you need to stop:
git rebase --abort
Rebase is powerful but rewrites history — never rebase commits that have already been pushed to a shared remote branch without coordinating with your team.
Practical Tips for Cleaner Conflict Resolution
Read both versions before editing. Understand what each developer was trying to accomplish. Many conflicts are resolved incorrectly because the developer just picked one version without understanding the other's intent.
Test after resolving. Always run your tests after resolving conflicts. A syntactically correct resolution can still introduce logical bugs if you combined two changes incorrectly.
Use git log to understand context. If you're unsure why a change was made, look at the commit history for the conflicting lines:
git log -p src/auth/login.js
Resolve conflicts in small, focused commits on your branch. If your branch has accumulated weeks of changes, split them into smaller commits before merging. This makes conflicts smaller and easier to reason about.
Never leave conflict markers in committed code. Set up a pre-commit hook or CI check that searches for <<<<<<< in your codebase. Committed conflict markers cause runtime errors and signal a broken merge process.
FAQ
Q: Can I prevent merge conflicts entirely?
Not entirely, but you can minimize them significantly. Keep feature branches short-lived, merge against the main branch frequently, break large files into smaller focused modules, and communicate with teammates about who is working on what. Smaller, more frequent integrations lead to smaller, more manageable conflicts.
Q: What if I accidentally commit a file with conflict markers?
Fix it immediately. Edit the file to remove the markers and write the correct code, then commit the fix with a clear message like fix: remove conflict markers from login.js. If the commit is on a shared branch, you may want to inform your team. Most CI pipelines should catch this before it happens if they're configured to run tests.
Q: What's the difference between a merge conflict and a rebase conflict?
Mechanically, they're the same — Git can't automatically reconcile two sets of changes to the same lines. The difference is context. A merge conflict happens all at once when combining two branch histories. A rebase conflict happens commit by commit as your changes are replayed on top of the target branch. Rebase conflicts can be easier to reason about because each conflict is tied to a specific commit and its intent.
Q: Should I use git merge or git rebase?
This depends on your team's workflow. Merge preserves the true history of how branches were integrated; it's safer for shared branches because it doesn't rewrite history. Rebase produces a cleaner, linear history that's easier to read but requires discipline about never rebasing shared commits. Many teams use rebase for local cleanup before a merge ("merge after rebase") to get the best of both.
Q: How do I resolve a conflict where one branch deleted a file and the other modified it?
Git will report this as a "deleted by us" or "deleted by them" conflict. Run git status to see which. You then decide: if the deletion should win, run git rm <filename>. If the modification should win, run git add <filename>. Stage your decision and complete the merge.
Q: Why does my merge tool show a "base" version?
A three-way merge tool shows the common ancestor (the base), your version, and the incoming version. The base is the last state of the file before the two branches diverged. It's extremely useful context — it shows you what each side changed relative to the original, making it much easier to understand the intent of both sets of changes and produce a correct resolution.
Merge conflicts are one of those Git skills that feel intimidating at first and routine within a few months. The key insight is that a conflict isn't Git failing — it's Git correctly identifying a decision that only a human can make. Once you internalize that, the markers stop looking like errors and start looking like exactly what they are: a request for your judgment.