Git Merge Conflicts: The Complete Guide to Resolving Them in 2026

Published February 12, 2026 · 30 min read

⚙ Related: Not sure whether to preserve branch context or keep history linear? Read Git Merge vs Rebase. Need to undo a pushed PR merge? Read Git Revert a Merge Commit.
⚙ Decision guide: Not sure whether to copy a fix or undo a bad change? Read Git Cherry-Pick vs Revert.

Merge conflicts are the most common source of frustration for Git users, yet they follow a simple, predictable pattern. Every conflict is Git telling you: "Two people changed the same thing and I do not know which version to keep." Once you understand how conflicts arise, how to read the conflict markers, and the handful of commands that resolve them, they stop being scary and become routine.

This guide covers every type of merge conflict you will encounter: during merges, rebases, cherry-picks, and stash pops. You will learn how to use merge tools, how git rerere can automate repeated resolutions, how to handle conflicts in binary and auto-generated files, and how to structure your team workflow to minimize conflicts in the first place.

Table of Contents

What Causes Merge Conflicts?

Git can automatically merge most changes. Conflicts only occur when Git cannot determine the correct result on its own. There are three situations that trigger a conflict:

1. Same Line, Different Changes

Two branches modify the same line in the same file. This is the most common type of conflict.

# On main branch, line 15 of config.js:
const MAX_RETRIES = 3;

# On feature branch, line 15 of config.js:
const MAX_RETRIES = 5;

# Git cannot decide: should MAX_RETRIES be 3 or 5?
# Result: CONFLICT

2. Edit vs Delete

One branch modifies a file while another branch deletes it entirely:

# On main: deleted utils/legacy-parser.js
# On feature: modified utils/legacy-parser.js (added new function)

$ git merge feature
CONFLICT (modify/delete): utils/legacy-parser.js deleted in HEAD
and modified in feature. Version feature of utils/legacy-parser.js
left in tree.

3. Competing Additions

Two branches add different content at the same location, or both create a file with the same name but different contents:

# Both branches added a new file: src/helpers/format.js
# but with completely different implementations

$ git merge feature
CONFLICT (add/add): Merge conflict in src/helpers/format.js

What Does NOT Cause Conflicts

Git is smart about merging. These situations resolve automatically:

Understanding Conflict Markers

When Git encounters a conflict, it writes special markers into the file to show both versions. Learning to read these markers is essential.

The Three-Part Marker

<<<<<<< HEAD
const apiUrl = "https://api.example.com/v2";
const timeout = 5000;
=======
const apiUrl = "https://api.example.com/v3";
const timeout = 10000;
>>>>>>> feature/upgrade-api

Here is what each section means:

The diff3 Format (Recommended)

The default two-way marker does not show what the code looked like before either branch changed it. The diff3 style adds a third section showing the common ancestor:

# Enable diff3 conflict style (highly recommended)
git config --global merge.conflictStyle diff3

Now conflicts look like this:

<<<<<<< HEAD
const apiUrl = "https://api.example.com/v2";
const timeout = 5000;
||||||| parent of abc1234
const apiUrl = "https://api.example.com/v1";
const timeout = 3000;
=======
const apiUrl = "https://api.example.com/v3";
const timeout = 10000;
>>>>>>> feature/upgrade-api

The ||||||| section shows the original code both branches started from. With this context, you can see that HEAD bumped the API to v2 and timeout to 5000, while the feature branch bumped to v3 and 10000. The resolution becomes clear: take v3 and decide on the timeout.

Multiple Conflicts in One File

A single file can have multiple conflict regions. Each one is independent and marked with its own set of markers:

import { useState } from 'react';
<<<<<<< HEAD
import { validateEmail } from './validators';
=======
import { validateEmail, validatePhone } from './validators';
>>>>>>> feature/phone-auth

// ... many lines of non-conflicting code ...

<<<<<<< HEAD
function handleSubmit(email) {
=======
function handleSubmit(email, phone) {
>>>>>>> feature/phone-auth

You must resolve every conflict region in the file before staging it.

Step-by-Step Conflict Resolution

Here is the exact process to follow every time you hit a conflict, regardless of whether it happened during a merge, rebase, or cherry-pick.

Step 1: Identify conflicted files

$ 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:   src/config.js
        both modified:   src/api/client.js
        deleted by us:   src/legacy/parser.js

Step 2: Open each conflicted file and resolve

For each file, find the conflict markers, decide what the final code should be, and remove all markers. Your resolution can be any of these:

# Before resolution (conflict markers present):
<<<<<<< HEAD
const MAX_CONNECTIONS = 10;
=======
const MAX_CONNECTIONS = 25;
>>>>>>> feature/scaling

# After resolution (markers removed, final code):
const MAX_CONNECTIONS = 25;

Step 3: Stage the resolved files

# Stage each file after resolving its conflicts
git add src/config.js
git add src/api/client.js

# For delete conflicts, choose to keep or remove
git add src/legacy/parser.js    # keep the file
git rm src/legacy/parser.js     # accept the deletion

Step 4: Complete the operation

# For merges: commit
git commit
# Git auto-generates a merge commit message

# For rebases: continue
git rebase --continue

# For cherry-picks: continue
git cherry-pick --continue

Step 5: Verify the result

# Check that no conflict markers remain
git diff --check

# Review what the merge produced
git log --oneline -5
git diff HEAD~1

Resolving Conflicts During Merge

A merge conflict is the simplest case. You are combining two branches and Git stops at the conflicting files.

# Start the merge
$ git merge feature/user-dashboard
Auto-merging src/components/Header.jsx
CONFLICT (content): Merge conflict in src/components/Header.jsx
Auto-merging src/styles/main.css
CONFLICT (content): Merge conflict in src/styles/main.css
Automatic merge failed; fix conflicts and then commit the result.

A realistic conflict in a React component:

# src/components/Header.jsx
import React from 'react';
<<<<<<< HEAD
import { Logo, NavMenu, SearchBar } from './ui';

export function Header({ user }) {
    return (
        <header className="header-main">
            <Logo />
            <NavMenu />
            <SearchBar />
            <span>{user.name}</span>
=======
import { Logo, NavMenu, UserAvatar } from './ui';

export function Header({ user, notifications }) {
    return (
        <header className="header-redesign">
            <Logo size="large" />
            <NavMenu />
            <UserAvatar user={user} />
            <span className="badge">{notifications.length}</span>
>>>>>>> feature/user-dashboard

To resolve this, you combine both changes: keep the SearchBar from HEAD, the UserAvatar and notifications from the feature branch, and use the redesigned class name:

# Resolved version:
import React from 'react';
import { Logo, NavMenu, SearchBar, UserAvatar } from './ui';

export function Header({ user, notifications }) {
    return (
        <header className="header-redesign">
            <Logo size="large" />
            <NavMenu />
            <SearchBar />
            <UserAvatar user={user} />
            <span className="badge">{notifications.length}</span>

Then stage and commit:

git add src/components/Header.jsx src/styles/main.css
git commit
# Editor opens with: "Merge branch 'feature/user-dashboard'"
# Save and close to complete the merge

Shortcut: Accept One Side Entirely

When you know you want to accept one side for an entire file, use checkout:

# Keep our version (discard their changes)
git checkout --ours src/styles/main.css
git add src/styles/main.css

# Keep their version (discard our changes)
git checkout --theirs src/config/database.yml
git add src/config/database.yml

For an entire merge where you want one side to win on all conflicts:

# Accept all of their changes on conflicts
git merge feature/branch -X theirs

# Accept all of our changes on conflicts
git merge feature/branch -X ours

Note: -X theirs still auto-merges non-conflicting changes from both sides. It only uses "theirs" as a tiebreaker for actual conflicts.

Resolving Conflicts During Rebase

Rebase conflicts work differently because rebase replays commits one at a time. You might hit a conflict at commit 3 of 8, resolve it, continue, and hit another at commit 5. This means the --ours and --theirs labels are swapped compared to merge.

During rebase, "ours" is the branch you are rebasing onto (main), and "theirs" is your feature branch commit being replayed. This is the opposite of merge and catches many developers off guard.

# Rebase your feature branch onto main
$ git rebase main
Applying: Add payment processing
Applying: Add order validation
CONFLICT (content): Merge conflict in src/checkout.js
error: could not apply c4d5e6f... Add order validation
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run
hint: "git rebase --continue".

Resolve the conflict just like a merge, but finish with --continue instead of commit:

# 1. Fix the conflict in src/checkout.js
# 2. Stage it
git add src/checkout.js

# 3. Continue the rebase (applies remaining commits)
git rebase --continue

# Git may stop again at the next conflict. Repeat until done.

Rebase --ours vs --theirs (Swapped!)

# During rebase: accept the main branch version (drop your change)
git checkout --ours src/checkout.js

# During rebase: accept your feature branch version
git checkout --theirs src/checkout.js

For a deep dive into rebase workflows, see our Git Rebase Complete Guide.

Resolving Conflicts During Cherry-Pick

Cherry-pick applies a single commit from another branch. Conflicts arise when that commit touches code that has diverged on your current branch.

# Cherry-pick a specific bugfix commit
$ git cherry-pick a1b2c3d
error: could not apply a1b2c3d... Fix null pointer in user lookup
hint: After resolving the conflicts, mark them with "git add"
hint: and run "git cherry-pick --continue"

The resolution process is identical to merge:

# 1. Resolve conflicts in the affected files
# 2. Stage resolved files
git add src/services/user-service.js

# 3. Continue the cherry-pick
git cherry-pick --continue

# Or abort if the cherry-pick is not worth the trouble
git cherry-pick --abort

Cherry-Picking Multiple Commits

# Cherry-pick a range (may conflict at each commit)
git cherry-pick a1b2c3d..f6g7h8i

# Cherry-pick specific commits
git cherry-pick a1b2c3d e4f5g6h

# If a conflict occurs, resolve it and continue
git cherry-pick --continue

Using Merge Tools

While you can resolve conflicts in any text editor, dedicated merge tools show both versions side by side (or in a three-way view) and let you click to accept changes. This is faster and less error-prone for complex conflicts.

VS Code

VS Code detects conflict markers automatically and adds inline buttons:

# Set VS Code as your merge tool
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait --merge $REMOTE $LOCAL $BASE $MERGED'

IntelliJ / WebStorm / JetBrains IDEs

JetBrains IDEs have a built-in three-way merge tool that shows the base version, your version, and their version in three panes with a result pane at the bottom:

# Set IntelliJ as your merge tool
git config --global merge.tool intellij
git config --global mergetool.intellij.cmd 'idea merge $LOCAL $REMOTE $BASE $MERGED'
git config --global mergetool.intellij.trustExitCode true

vimdiff

For terminal users, vimdiff shows conflicts in split panes:

# Set vimdiff as your merge tool
git config --global merge.tool vimdiff
git config --global mergetool.vimdiff.cmd 'vim -d $LOCAL $REMOTE $MERGED'

# vimdiff keyboard shortcuts:
# ]c      - jump to next conflict
# [c      - jump to previous conflict
# :diffget LOCAL  - accept left (local) version
# :diffget REMOTE - accept right (remote) version
# :wqa    - save all and quit

Meld

Meld is a popular graphical diff and merge tool for Linux:

# Install meld (Debian/Ubuntu)
sudo apt install meld

# Set as merge tool
git config --global merge.tool meld
git config --global mergetool.meld.cmd 'meld $LOCAL $BASE $REMOTE --output $MERGED'

The git mergetool Command

Once you have configured a merge tool, git mergetool walks you through each conflicted file one at a time, launching your tool for each one:

$ git mergetool
Merging:
src/config.js
src/api/client.js

Normal merge conflict for 'src/config.js':
  {local}: modified file
  {remote}: modified file
Hit return to start merge resolution tool (vscode):

After you resolve and save in the tool, Git asks if the merge was successful, then moves to the next file. When all files are done:

# mergetool creates .orig backup files. Clean them up:
git clean -f *.orig

# Or prevent .orig files from being created:
git config --global mergetool.keepBackup false

# Stage and commit
git add .
git commit

Useful mergetool Options

# Only resolve specific files
git mergetool src/config.js

# Use a different tool for one invocation
git mergetool --tool=meld

# List available tools
git mergetool --tool-help

Aborting a Merge

Sometimes the best move is to stop, think, and try again later. Every Git operation with conflicts has a --abort flag that cleanly restores the pre-conflict state:

# Abort a merge
git merge --abort

# Abort a rebase
git rebase --abort

# Abort a cherry-pick
git cherry-pick --abort

# Abort a stash pop (revert to pre-stash state)
git checkout -- .
git clean -fd

Aborting is always safe. It does not lose any previous commits or changes. It simply restores your working directory and index to the state before the operation started.

When to Abort

Reset After a Completed Merge

If you already committed the merge but realize it was wrong:

# Undo the last commit (the merge commit), keep changes staged
git reset --soft HEAD~1

# Or undo completely (discard merge changes)
git reset --hard HEAD~1

# Or revert the merge (creates a new commit that undoes it)
git revert -m 1 HEAD

Preventing Merge Conflicts

The best conflict is one that never happens. These strategies drastically reduce conflict frequency:

1. Keep Branches Short-Lived

The longer a branch lives, the more it diverges from main, and the more likely conflicts become. Aim for branches that last 1-3 days, not weeks.

2. Pull and Rebase Frequently

# Do this daily (or more often) on your feature branch
git fetch origin
git rebase origin/main

# Or set pull to rebase by default
git config --global pull.rebase true

3. Use Consistent Code Formatting

Many conflicts come from formatting differences: tabs vs spaces, trailing whitespace, line endings. Use automated formatters:

# JavaScript/TypeScript: Prettier
npx prettier --write .

# Python: Black
black .

# Go: gofmt (built in)
gofmt -w .

# Set up pre-commit hooks to format automatically
# See our Git Hooks Complete Guide

4. Break Up Large Files

A 2000-line file is a conflict magnet. If multiple developers need to touch it regularly, refactor it into smaller, focused modules.

5. Communicate About Shared Code

If you know a teammate is working on the same file, coordinate. A quick message saying "I am refactoring the auth module today" prevents duplicate work and conflicts.

6. Use .gitattributes for Merge Strategies

# .gitattributes
# Always accept ours for lock files
package-lock.json merge=ours
yarn.lock merge=ours

# Use union merge for changelog (append both changes)
CHANGELOG.md merge=union

Advanced: git rerere (Reuse Recorded Resolution)

rerere stands for "reuse recorded resolution." When enabled, Git records how you resolve each conflict. If the same conflict appears again (common during repeated rebases), Git applies your previous resolution automatically.

# Enable rerere globally
git config --global rerere.enabled true

How It Works

# First time: you resolve a conflict manually
$ git merge feature/api-v2
CONFLICT: src/api/client.js
# You resolve it and commit

# Git records the resolution in .git/rr-cache/

# Later: you abort the merge, update main, and merge again
$ git merge feature/api-v2
Resolved 'src/api/client.js' using previous resolution.
# The same conflict is auto-resolved!

rerere Commands

# See what rerere has recorded
git rerere status

# Show the diff rerere would apply
git rerere diff

# Forget a specific resolution (if it was wrong)
git rerere forget src/api/client.js

# Clear all recorded resolutions
git rerere gc

When rerere Saves You

Conflicts in Special File Types

Binary Files

Git cannot merge binary files (images, compiled assets, PDFs). It simply reports the conflict and you must choose one version:

$ git merge feature/new-logo
CONFLICT (content): Merge conflict in assets/logo.png

# You must choose one version:
git checkout --ours assets/logo.png    # keep current
git checkout --theirs assets/logo.png  # take incoming
git add assets/logo.png

For binary assets, use Git LFS (Large File Storage) and establish a process where one person owns each asset at a time.

Lock Files (package-lock.json, yarn.lock)

Lock files are auto-generated and should not be hand-merged. Instead, accept one version and regenerate:

# For npm (package-lock.json)
git checkout --theirs package-lock.json
npm install
git add package-lock.json

# For yarn (yarn.lock)
git checkout --theirs yarn.lock
yarn install
git add yarn.lock

# For pip (requirements.txt generated by pip-compile)
git checkout --theirs requirements.txt
pip-compile requirements.in
git add requirements.txt

Auto-Generated Files

Files generated by build tools, schema generators, or ORMs should be regenerated rather than hand-merged:

# Accept either version, then regenerate
git checkout --theirs prisma/schema.prisma
npx prisma generate
git add prisma/

Preventing Lock File Conflicts with .gitattributes

# .gitattributes
# Mark lock files as binary so Git does not attempt line-by-line merge
package-lock.json binary
yarn.lock binary

# Or use a custom merge driver that always regenerates
package-lock.json merge=npm-lock

Team Workflows to Minimize Conflicts

CODEOWNERS for Coordination

GitHub and GitLab support CODEOWNERS files that assign reviewers based on file paths. This naturally reduces conflicts by making ownership explicit:

# .github/CODEOWNERS
/src/auth/          @auth-team
/src/payments/      @payments-team
/src/components/ui/ @design-system-team
*.sql               @dba-team

Feature Flags Over Long Branches

Instead of keeping a large feature on a branch for weeks, merge small incremental changes behind a feature flag:

# Merge to main daily, hidden behind a flag
if (featureFlags.newCheckout) {
    renderNewCheckout();
} else {
    renderLegacyCheckout();
}
// Small, frequent merges = fewer conflicts

Trunk-Based Development

The ultimate conflict-prevention strategy: everyone commits to main (or very short-lived branches). Conflicts are rare because branches diverge minimally. See our Git Branching Strategies Guide for implementation details.

Recommended Team Git Configuration

# Every team member should run:
git config --global merge.conflictStyle diff3
git config --global rerere.enabled true
git config --global pull.rebase true
git config --global mergetool.keepBackup false

Common Scenarios and Solutions

Scenario 1: Conflicting Package Versions

# package.json conflict:
<<<<<<< HEAD
    "lodash": "^4.17.21",
    "axios": "^1.6.0",
=======
    "lodash": "^4.17.21",
    "axios": "^1.7.2",
>>>>>>> feature/upgrade-deps

# Resolution: take the higher version
    "lodash": "^4.17.21",
    "axios": "^1.7.2",

# Then regenerate the lock file:
npm install

Scenario 2: Both Branches Add to the Same List

# routes.js conflict:
<<<<<<< HEAD
app.get('/api/users', usersController.list);
app.get('/api/users/:id', usersController.get);
app.post('/api/users', usersController.create);
=======
app.get('/api/users', usersController.list);
app.get('/api/users/:id', usersController.get);
app.get('/api/products', productsController.list);
>>>>>>> feature/products

# Resolution: keep all routes
app.get('/api/users', usersController.list);
app.get('/api/users/:id', usersController.get);
app.post('/api/users', usersController.create);
app.get('/api/products', productsController.list);

Scenario 3: Refactored File vs Modified File

# You refactored utils.js into utils/string.js and utils/number.js
# A teammate added a new function to the old utils.js

$ git merge feature/new-helpers
CONFLICT (modify/delete): utils.js deleted in HEAD and modified
in feature/new-helpers.

# Solution: take their new function and put it in the right module
git show feature/new-helpers:utils.js  # view their version
# Copy the new function into utils/string.js or utils/number.js
git rm utils.js
git add utils/
git commit

Scenario 4: Conflicting Database Migrations

# Two branches created migration files with the same sequence number
# migrations/0042_add_email_field.sql (yours)
# migrations/0042_add_phone_field.sql (theirs)

# Solution: renumber one migration
mv migrations/0042_add_phone_field.sql migrations/0043_add_phone_field.sql
# Update any migration dependency references
git add migrations/

Scenario 5: Stash Pop Conflict

$ git stash pop
CONFLICT (content): Merge conflict in src/app.js

# The stash is NOT dropped when there is a conflict
# Resolve the conflict, then manually drop the stash
git add src/app.js
git stash drop

Frequently Asked Questions

What causes a git merge conflict?

A git merge conflict occurs when two branches modify the same line in the same file, or when one branch deletes a file that another branch has modified. Git cannot automatically determine which change to keep, so it pauses the merge and asks you to resolve the conflict manually by editing the file and choosing the correct content.

How do I resolve a merge conflict in Git?

Open the conflicted file and look for conflict markers (<<<<<<< HEAD, =======, >>>>>>>). The content between <<<<<<< HEAD and ======= is your current branch's version. The content between ======= and >>>>>>> is the incoming branch's version. Edit the file to keep the code you want, remove all conflict markers, then run git add on the file and git commit to complete the merge.

How do I abort a merge conflict in Git?

Run git merge --abort to cancel the merge and return your working directory to the state before the merge started. For rebase conflicts, use git rebase --abort. For cherry-pick conflicts, use git cherry-pick --abort. These commands are safe and will not lose any of your previous work.

What is git rerere and how does it help with merge conflicts?

git rerere (reuse recorded resolution) is a feature that records how you resolve a conflict and automatically applies the same resolution if the same conflict occurs again. Enable it with git config --global rerere.enabled true. It is especially useful when rebasing frequently, as you may encounter the same conflict multiple times across different commits.

How do I prevent merge conflicts in a team?

Keep feature branches short-lived (1-3 days). Pull and rebase from main frequently. Break large files into smaller modules so team members edit different files. Use code ownership and CODEOWNERS files. Coordinate with teammates when working on the same area. Use consistent code formatting with tools like Prettier or Black to avoid whitespace-only conflicts.

Continue Learning

This guide is part of our Git deep-dive series. Explore the related guides to master every aspect of Git:

Related Tools

Git Diff Viewer
Visualize and compare diffs in the browser
Git Cheat Sheet
One-page quick reference for all Git commands
JSON Formatter
Format and validate JSON data instantly
Regex Tester
Test regular expressions with live matching