What is a Git Merge Conflict?
A merge conflict occurs when Git cannot automatically reconcile differences between two commits during a merge operation. This typically happens when two branches have modified the same line (or adjacent lines) in a file, or when one branch deletes a file while another modifies it. Git pauses the merge and asks you, the developer, to manually resolve the discrepancies.
Behind the scenes, Git tries to combine the snapshots of your current branch (HEAD) and the branch you're merging (MERGE_HEAD). If the changes affect different parts of the same file, Git merges them cleanly. If they overlap, Git marks the conflicting area and leaves it for you to fix.
Why Understanding Merge Conflicts Matters
Conflicts are a normal part of collaborative development. Ignoring them or resolving them incorrectly can introduce bugs, break tests, or lose important work. Mastering conflict resolution lets you integrate features safely, maintain code quality, and keep your team's history coherent. It also reduces fear around merging, encouraging smaller, more frequent integrations.
Anatomy of a Conflict Marker
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →When a conflict arises, Git inserts markers into the affected file to show the conflicting regions. A typical conflict block looks like this:
<<<<<<< HEAD
def greet():
print("Hello from main branch!")
=======
def greet():
print("Hello from feature branch!")
>>>>>>> feature-branch
The markers are:
<<<<<<<– Start of the conflict; followed by the branch name (hereHEAD).=======– Separates the two conflicting versions.>>>>>>>– End of the conflict; followed by the branch name being merged.
Everything between <<<<<<< and ======= is the version from your current branch (HEAD). The content between ======= and >>>>>>> is the version from the branch you are merging.
Step-by-Step Conflict Resolution
Step 1: Initiate the Merge and Detect Conflicts
Start a merge from your current branch (e.g., main) into a feature branch, or vice versa. If conflicts occur, Git will output a message and pause.
git checkout main
git merge feature-branch
# Auto-merging app.js
# CONFLICT (content): Merge conflict in app.js
# Automatic merge failed; fix conflicts and then commit the result.
Step 2: View the List of Conflicted Files
Use git status to see which files are in conflict. Files listed as both modified need resolution.
git status
# On branch main
# You have unmerged paths.
# (fix conflicts and run "git commit")
#
# Unmerged paths:
# (use "git add <file>..." to mark resolution)
#
# both modified: app.js
#
# no changes added to commit (use "git add" and/or "git commit -a")
Step 3: Open the Conflicted File
Open the file in your editor. You will see the conflict markers exactly as shown earlier. The goal is to replace the entire block (from <<<<<<< to >>>>>>>) with the final, correct code.
// Inside app.js
<<<<<<< HEAD
console.log("Hello from main branch!");
=======
console.log("Hello from feature branch!");
>>>>>>> feature-branch
Step 4: Choose a Resolution Strategy
You have several options:
- Manual editing – Combine both changes, pick one, or rewrite the section entirely.
- Accept one version entirely – Keep either the
HEADversion or the merged branch version. - Use a merge tool – Visual diff tools help you pick and choose changes interactively.
Step 5: Edit the File to Resolve
For manual resolution, delete the markers and write the desired code. For example, you might decide to combine both log messages:
// Resolved version
console.log("Hello from main branch!");
console.log("Hello from feature branch!");
If you want to keep only the version from the feature branch, simply remove the HEAD block and the markers, leaving only the feature-branch line. There should be no leftover markers.
Step 6: Mark the File as Resolved
After saving the file, tell Git that the conflict is resolved by staging it with git add.
git add app.js
Run git status again to confirm the file is no longer listed under unmerged paths. It should appear in Changes to be committed.
Step 7: Complete the Merge Commit
Once all conflicted files are staged, finish the merge by committing. Git will supply a default merge commit message.
git commit
# (opens editor with merge message, then save and exit)
Alternatively, you can use git commit -m "Merge feature-branch: resolve conflicts in app.js".
Using Git Quick Commands to Accept One Side
If you want to keep one branch's version entirely for a particular file, you can use:
git checkout --ours <file>– Keep the version from the current branch (HEAD).git checkout --theirs <file>– Keep the version from the branch being merged.
After running one of these, you still need to stage the file with git add.
# Keep the feature-branch version of the file
git checkout --theirs app.js
git add app.js
Note: --ours and --theirs are reversed during a rebase. In a rebase, --ours refers to the branch you are rebasing onto, while --theirs refers to the commits you are replaying. Always double-check which side is which during a rebase conflict.
Using a Merge Tool
Git integrates with various visual diff tools (like VSCode, Meld, Beyond Compare, or KDiff3). To launch a configured merge tool:
git mergetool
This opens a three-pane view (BASE, LOCAL, REMOTE) for each conflicted file, allowing you to pick changes visually. After saving, the tool automatically stages the file. You can also configure your preferred tool globally:
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'
Practical Example: Resolving a Real Conflict
Let's simulate a conflict from scratch. We'll create a repository, make divergent changes in two branches, and resolve them.
# Setup
mkdir conflict-demo && cd conflict-demo
git init
echo "function calculateTotal(price, tax) {" > util.js
echo " return price + tax;" >> util.js
echo "}" >> util.js
git add util.js && git commit -m "Initial implementation"
# Create feature branch and modify the same function
git checkout -b feature/add-discount
# Edit util.js to apply discount
sed -i 's/return price + tax;/return (price - discount) + tax;/' util.js
# Also add a 'discount' parameter
sed -i 's/function calculateTotal(price, tax)/function calculateTotal(price, tax, discount)/' util.js
git add util.js && git commit -m "Add discount parameter"
# Switch back to main and modify the function differently
git checkout main
# Edit util.js to add currency conversion
sed -i 's/return price + tax;/return (price + tax) * exchangeRate;/' util.js
sed -i 's/function calculateTotal(price, tax)/function calculateTotal(price, tax, exchangeRate)/' util.js
git add util.js && git commit -m "Add exchange rate support"
# Merge feature branch into main – CONFLICT!
git merge feature/add-discount
Now util.js contains conflict markers. Let's inspect it:
<<<<<<< HEAD
function calculateTotal(price, tax, exchangeRate) {
return (price + tax) * exchangeRate;
=======
function calculateTotal(price, tax, discount) {
return (price - discount) + tax;
>>>>>>> feature/add-discount
We need to combine both features: discount and exchange rate. We'll edit the file manually:
// Resolved version
function calculateTotal(price, tax, discount, exchangeRate) {
return ((price - discount) + tax) * exchangeRate;
}
After saving, stage and commit:
git add util.js
git commit -m "Merge feature/add-discount: combine discount and exchange rate"
The merge is complete. Verify with git log --oneline and git status.
Best Practices for Avoiding and Handling Conflicts
-
Pull frequently – Regularly update your feature branch with changes from the main branch (
git pull --rebase) to reduce the divergence window. - Keep commits small and focused – Small, atomic commits make it easier to understand changes and resolve conflicts incrementally.
- Communicate with your team – If you're modifying a shared area, coordinate with others to avoid stepping on each other's toes.
- Use short-lived feature branches – Long-running branches accumulate more changes, increasing conflict likelihood and complexity.
- Test after resolution – Always run your test suite after resolving conflicts to ensure the merged code behaves as expected.
-
Configure merge strategies – Use
git merge --no-fffor explicit merge commits, or considerrerere(git config --global rerere.enabled true) to record and reuse conflict resolutions. -
Don't commit conflict markers – Before staging, use
git diffto verify no markers remain. Tools like pre-commit hooks can catch leftover markers. -
Abort if overwhelmed – If a merge gets too messy, cancel it with
git merge --abortand revisit your strategy.
Conclusion
Git merge conflicts are a natural part of collaborative software development. By understanding conflict markers, following a structured resolution workflow, and leveraging Git's built-in helpers and merge tools, you can turn a potentially frustrating situation into a controlled, routine process. The key is to stay calm, read the markers carefully, and always test the final result. With practice and the best practices outlined here, you'll be able to resolve conflicts quickly, maintain a clean history, and keep your team moving forward without fear of merges.