← Back to Blog

Git Merge Conflicts: How to Resolve Every Type Like a Senior Developer

Merge conflicts are not bugs. They are Git telling you that two people changed the same thing and it needs a human decision. Once you understand what is actually happening, resolving them takes seconds instead of panic.

TL;DR

Open the conflicted file, find <<<<<<< markers, choose which code to keep, remove the markers, then run git add and git commit. For VS Code users: just click "Accept Current Change", "Accept Incoming Change", or "Accept Both Changes" above each conflict block.

What Merge Conflicts Actually Are

Here is the scenario. You are working on a feature branch. Your teammate is working on their own branch. Both of you happen to edit the same line in the same file. When you try to merge, Git cannot figure out which version to keep. So it stops, marks the conflicting sections, and asks you to sort it out.

Think of it like two editors working on the same paragraph of a document at the same time. Editor A changes "the blue car" to "the red car." Editor B changes "the blue car" to "the green truck." When you try to combine their work, you have a conflict. Someone needs to decide: is it a red car, a green truck, or something else entirely?

Visually, imagine your Git history looking like this:

       A---B---C  (feature-branch)
      /
 D---E---F---G  (main)
      \
       H---I  (teammate-branch)

If commits C and I both modified the same lines in the same file, merging feature-branch and teammate-branch into main will produce a conflict. Git is smart enough to auto-merge changes to different files, or even different sections of the same file. Conflicts only happen when two branches touch the exact same lines.

Reading Conflict Markers

When a conflict occurs, Git inserts special markers into the file to show you both versions. These markers look intimidating the first time you see them, but they are actually straightforward once you know the pattern.

function getDiscount(user) {
<<<<<<< HEAD
  if (user.isPremium) {
    return 0.20;
  }
=======
  if (user.membershipLevel === 'gold') {
    return 0.25;
  }
>>>>>>> feature/loyalty-tiers
  return 0;
}

Let me break this down:

  • <<<<<<< HEAD marks the start of YOUR version (the branch you are currently on, the one you are merging INTO)
  • ======= is the divider between the two versions
  • >>>>>>> feature/loyalty-tiers marks the end of THEIR version (the branch you are merging FROM)

Everything between <<<<<<< and ======= is your current branch code. Everything between ======= and >>>>>>> is the incoming branch code. Your job is to decide what the final version should look like, then delete all three marker lines.

Pro tip: if you run git diff during a conflict, Git uses a combined diff format showing both sides. But honestly, just open the file. It is easier to read in context.

Step-by-Step Resolution (Manual Editing)

Let me walk through the entire process from start to finish. This is what you will do 90% of the time.

Step 1: Identify Conflicted Files

After a failed merge, Git tells you which files have conflicts. You can also check anytime with:

git status

You will see something like:

Unmerged paths:
  (use "git add <file>..." to mark resolution)
        both modified:   src/utils/pricing.js
        both modified:   src/components/Header.tsx

Step 2: Open the File and Find Markers

Open each conflicted file and search for <<<<<<<. Every conflict block will have this marker. A single file can have multiple conflict blocks if several sections were modified by both branches.

Step 3: Decide What to Keep

For each conflict block, you have four options:

  1. Keep your version (delete the incoming code and all markers)
  2. Keep their version (delete your code and all markers)
  3. Keep both (delete only the markers, keep all the code)
  4. Write something new (delete everything and write a combined solution)

Using our earlier example, option 4 might look like this:

function getDiscount(user) {
  if (user.membershipLevel === 'gold') {
    return 0.25;
  }
  if (user.isPremium) {
    return 0.20;
  }
  return 0;
}

Notice: no markers remain. The file is clean, valid code that combines both intents.

Step 4: Stage and Commit

# Mark the file as resolved
git add src/utils/pricing.js
git add src/components/Header.tsx

# Complete the merge
git commit

Git will auto-populate a merge commit message like "Merge branch 'feature/loyalty-tiers' into main." You can edit it or accept it as-is.

Resolving Conflicts in VS Code

VS Code has excellent built-in merge conflict support. When you open a file with conflicts, VS Code highlights each conflict block with colored backgrounds and adds clickable buttons above each one:

  • Accept Current Change (keep your version, shown in green)
  • Accept Incoming Change (keep their version, shown in blue)
  • Accept Both Changes (keep everything, remove only markers)
  • Compare Changes (open a side-by-side diff view)

For complex conflicts, use VS Code's 3-way merge editor. Open it from the Source Control panel by clicking the merge icon next to a conflicted file. This shows three panes: your version (Current), their version (Incoming), and the result at the bottom. You can check/uncheck individual changes to build the final result. It is the closest thing to a visual drag-and-drop conflict resolution tool.

After resolving all conflicts in a file, save it, then stage it in the Source Control panel (or run git add in the terminal). Once all files are resolved and staged, commit. VS Code will suggest the default merge commit message.

Resolving Conflicts in the CLI

If you prefer the terminal, Git has built-in support for external merge tools.

Using git mergetool

# Launch the configured merge tool for all conflicted files
git mergetool

# Or specify a tool directly
git mergetool --tool=vimdiff

Common merge tools you can configure:

  • vimdiff: built into most systems, 4-pane view (LOCAL, BASE, REMOTE, MERGED)
  • opendiff (macOS): Apple's FileMerge, clean graphical 3-way merge
  • meld: cross-platform, very popular on Linux, great visual diffs
  • kdiff3: cross-platform, automatic merge with manual override

To set your default merge tool permanently:

git config --global merge.tool vimdiff
git config --global mergetool.keepBackup false

That second line prevents Git from leaving .orig backup files everywhere after resolving conflicts.

Quick CLI Resolution Without a Tool

Sometimes you know upfront that you want to accept one side entirely. Git has shortcuts for that:

# Accept YOUR version for a specific file
git checkout --ours src/utils/pricing.js

# Accept THEIR version for a specific file
git checkout --theirs src/utils/pricing.js

# Then stage it
git add src/utils/pricing.js

Common Conflict Scenarios (and How to Handle Each)

Scenario 1: Same Line Edited Differently

This is the classic conflict. Both branches changed the same line. Open the file, read both versions, decide which one is correct (or combine them), remove markers, stage, commit. We covered this above.

Scenario 2: File Deleted on One Branch, Modified on Another

You deleted a file on your branch. Your teammate edited it on theirs. Git does not know if you want the deletion or the modification. Here is how to handle it:

# To keep the deletion (remove the file)
git rm src/old-component.tsx
git add .

# To keep the modified version (undo your deletion)
git checkout --theirs src/old-component.tsx
git add src/old-component.tsx

Scenario 3: Binary File Conflicts

Binary files (images, PDFs, compiled assets) cannot be merged line by line. Git will tell you there is a conflict but it cannot show you markers inside a PNG file. You have to pick one version:

# Keep your version of the image
git checkout --ours assets/logo.png

# Or keep their version
git checkout --theirs assets/logo.png

git add assets/logo.png

This is why many teams use Git LFS (Large File Storage) for binary assets. It reduces conflict frequency because LFS tracks pointers instead of full binary content.

Scenario 4: package-lock.json / yarn.lock Conflicts

This is the one that trips up junior developers constantly. Lock files are auto-generated and often have thousands of lines. Do NOT try to manually merge them. Here is the correct approach:

# Accept the target branch version of the lock file
git checkout --theirs package-lock.json

# Regenerate it based on the merged package.json
npm install

# Stage both files
git add package-lock.json package.json

git commit

If you are using Yarn, replace npm install with yarn install. The key insight is that the lock file is derived from package.json, so after merging package.json correctly, you regenerate the lock file from scratch.

Pro Tip: git rerere (Reuse Recorded Resolution)

This is one of those Git features that most developers never learn about, but it is incredibly useful if you deal with long-lived branches or frequent rebasing.

git rerere stands for "reuse recorded resolution." When enabled, Git remembers how you resolved a particular conflict. If the same conflict appears again (say, during a rebase or cherry-pick), Git automatically applies your previous resolution.

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

That is it. Once enabled, Git silently records every conflict resolution you make. The next time it encounters the same conflict pattern, it resolves it automatically. You will see a message like:

Resolved 'src/utils/pricing.js' using previous resolution.

This is especially powerful when you are rebasing a feature branch onto an updated main branch repeatedly. Without rerere, you would resolve the same conflicts every single rebase. With rerere, you resolve once and Git handles the rest. If you use our Diff Checker to compare branches before merging, you can often anticipate conflicts ahead of time.

Merge vs Rebase vs Squash: When to Use Each

Understanding these three strategies helps you choose the right approach for your workflow and minimize conflict headaches.

Strategy How It Works Conflict Risk Best For
Merge Creates a merge commit that combines both branches. Preserves full history. Low (one-time resolution) Feature branches merging into main. Team collaboration.
Rebase Replays your commits on top of the target branch. Creates linear history. Medium (may need to resolve per-commit) Keeping feature branches up to date. Clean commit history.
Squash Combines all branch commits into a single commit on the target. Low (single resolution point) Small features, bug fixes. When individual commits are not meaningful.

Here is the practical advice. If your team values clean, linear history, use rebase for updating feature branches and squash-merge when completing PRs. If you prefer preserving the full commit graph, use regular merges. Most teams end up with a hybrid: rebase locally, squash-merge on PR completion. Check out our Git Rebase vs Merge deep dive for more on this topic.

How to Prevent Merge Conflicts

The best conflict is the one that never happens. Here are the strategies senior developers use to minimize conflicts.

1. Keep Pull Requests Small

A PR that changes 5 files has a much lower chance of conflicting with someone else's work than a PR that changes 50 files. Aim for PRs that can be reviewed in under 30 minutes. Break large features into smaller, incremental PRs.

2. Rebase Frequently

If your feature branch lives for more than a day or two, rebase it onto the latest main regularly:

git fetch origin
git rebase origin/main

This keeps your branch close to main, so by the time you open a PR, the diff is small and conflicts are minimal. It is much easier to resolve one small conflict during a daily rebase than a massive tangle after two weeks of divergence.

3. Communicate With Your Team

If you know you are about to refactor a heavily-used utility file, tell your teammates. A quick Slack message like "I am rewriting pricing.js today, heads up if you are touching that area" saves everyone time. This sounds obvious, but it is the single most effective conflict prevention strategy.

4. Use a Branching Strategy

Whether it is GitFlow, trunk-based development, or GitHub Flow, having a clear branching strategy reduces surprises. The key principle: short-lived feature branches that merge back quickly. Long-lived branches are conflict magnets. Read more about Git best practices in our cheat sheet.

5. Configure .gitattributes for Auto-Merge Behavior

You can tell Git how to handle specific file types during merges:

# .gitattributes
# Always use the current branch version for lock files
package-lock.json merge=ours
yarn.lock merge=ours

# Mark binary files
*.png binary
*.jpg binary
*.pdf binary

Common Mistakes (and How to Avoid Them)

1. Deleting the Wrong Code

In the rush to "just fix the conflict," developers sometimes keep the wrong version. Always read both sides carefully. If you are not sure which version is correct, ask the person who wrote the other side. A 30-second Slack message is better than deploying broken code.

2. Leaving Conflict Markers in the Code

This is embarrassingly common. You think you resolved everything, but somewhere in the file there is still a <<<<<<< or ======= hiding. The code will not compile (usually), but if it is in a non-compiled file like HTML or CSS, it can slip into production. Always search the file for <<< after resolving.

3. Force Pushing After a Bad Merge

You messed up a merge, so you git push --force to "fix" it. Now your teammates' local branches are out of sync and they will get even more conflicts. If you need to undo a bad merge, use git revert instead. It creates a new commit that undoes the merge without rewriting history. Learn more in our guide to undoing commits.

4. Not Pulling Before Starting Work

Every coding session should start with:

git pull origin main

Or if you are on a feature branch:

git fetch origin
git rebase origin/main

Working on stale code is the number one cause of unnecessary conflicts.

5. Resolving Conflicts Without Understanding the Code

Sometimes the conflict is in code you did not write and do not understand. Do not just randomly pick a side. Read the PR description for context. Look at the commit messages. If you still do not understand the intent, reach out to the author. Blindly resolving a conflict can introduce subtle bugs that are hard to trace later.

6. Not Testing After Resolution

After resolving conflicts, always run your test suite before committing. The resolution might be syntactically valid but logically wrong. A merged function that returns both a discount percentage AND a loyalty tier string is technically "resolved" but completely broken. Run npm test, pytest, or whatever your project uses before pushing.

Compare Code Before You Merge

Use the SecureBin Diff Checker to compare two code versions side by side. Spot differences before they become conflicts.

Open Diff Checker

Frequently Asked Questions

Can I abort a merge if the conflicts are too complex?

Yes. Run git merge --abort to cancel the merge and return your branch to the state it was in before you started. Nothing is lost. You can also use git reset --merge if you have already started resolving some files but want to start over. This is completely safe and will not affect your commit history or the other branch.

Why does Git say there is a conflict when I did not change that file?

This usually happens because of whitespace changes, line ending differences (CRLF vs LF), or auto-formatting tools. Git tracks every character, so even invisible changes like trailing spaces or different line endings count as modifications. Check your .gitattributes file and consider setting core.autocrlf to handle line endings consistently across your team. Also check if someone has a code formatter (like Prettier) that reformats files on save.

How do I resolve conflicts in package-lock.json or yarn.lock?

Do not try to manually merge lock files. Accept either version entirely (usually the target branch version with git checkout --theirs package-lock.json), then run npm install or yarn install to regenerate the lock file. This ensures the lock file is consistent with your merged package.json. Attempting to hand-merge a lock file almost always creates an invalid or inconsistent dependency tree.

Wrapping Up

Merge conflicts look scary, but they are one of the most routine parts of working with Git. The process is always the same: find the markers, understand both versions, decide what the code should look like, remove the markers, and commit. With practice, you will resolve most conflicts in under a minute.

The real skill is not in resolving conflicts. It is in preventing them through good habits: small PRs, frequent rebasing, clear team communication, and a solid branching strategy. Enable git rerere to save yourself from repetitive resolutions, and learn your editor's merge tools so you can work efficiently.

If you work with cherry-picks or need to stash changes before resolving, check out those guides for more context. And for spotting differences between files before they cause merge issues, our Diff Checker and Exposure Checker are always free to use.

Related guides: Git Cheat Sheet 2026, Git Rebase vs Merge, Git Bisect, Undo Last Commit, .gitignore Best Practices, and GitHub Actions Guide.

UK
Written by Usman Khan
DevOps Engineer | MSc Cybersecurity | CEH | AWS Solutions Architect

Usman has 10+ years of experience securing enterprise infrastructure, managing high-traffic servers, and building zero-knowledge security tools. Read more about the author.