Topic · Branching & Integration

Merge Conflicts

A merge conflict is Git refusing to guess. When two branches changed the same lines, or one changed a file the other deleted, Git stops and asks you to choose. The conflict markers are not a failure mode; they are an invitation to make a decision Git cannot make on your behalf.

When conflicts happen

Git tries hard to merge silently. It looks at the common ancestor of the two branches, then at the changes each branch made on top of that ancestor, and combines them whenever the edits land in different places. Non-overlapping edits to the same file? Merged. Edits to different files? Merged. Identical edits made on both sides? Merged. The conflict appears only when both branches reached for the same content and made incompatible decisions about it.

Four situations produce a conflict in practice:

  • Both branches changed the same lines of the same file. The most common case, and the one the conflict markers are designed for.
  • One branch modified a file the other deleted. Git cannot decide whether the deletion or the edit reflects your real intent.
  • Both branches added a file with the same path but different content. Git refuses to silently pick a winner.
  • Rename or mode conflicts. One side renamed a file, the other modified it under the old name; or the executable bit differs across branches.

If you remember nothing else from this section, remember the principle: conflicts are local to the spots where humans on both sides made independent decisions about the same bytes. Everything else merges automatically.

A conflict is the spot where two histories disagree Base commit title = "Hello" ours (main) title = "Welcome" theirs (feature) title = "Hi there" Conflict same line, two values Both branches edited the same line starting from the same base. Git refuses to pick.

What you see in a conflicted file

When a merge stops with a conflict, two things change. First, git status grows an "Unmerged paths" section listing every file Git could not auto-resolve. Second, each conflicted file is rewritten on disk with conflict markers around the disputed regions. The rest of the file is left untouched.

git status outputshell
$ git merge feature/new-button
Auto-merging src/header.js
CONFLICT (content): Merge conflict in src/header.js
Automatic merge failed; fix conflicts and then commit the result.

$ git status
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/header.js

Open the file and you will find Git's marker syntax wrapped around every disputed hunk. HEAD is the branch you were on when you started the merge; the branch name after the closing marker is the one you are merging in:

src/header.js (conflicted)javascript
export function Header() {
  return (
<<<<<<< HEAD
    <h1 className="page-title">Welcome</h1>
=======
    <h1 className="hero">Hi there</h1>
>>>>>>> feature/new-button
  );
}

The three marker lines partition the region into "ours" (between <<<<<<< and =======) and "theirs" (between ======= and >>>>>>>). To resolve, you edit the file so the final content is what you want and you delete all three marker lines. That is the entire syntactic requirement; the semantic work is the choice you make.

The same file after a thoughtful resolution might look like this:

src/header.js (resolved)javascript
export function Header() {
  return (
    <h1 className="hero">Welcome</h1>
  );
}

Notice that the resolution is not necessarily either side: this version takes the text from "ours" and the class name from "theirs". The merge is whatever you decide makes the project correct, and conflict markers exist only to make that decision visible.

The index has three stages during a conflict

Behind the marker lines in your working file, Git keeps a more structured view of the conflict in the index. While the merge is in progress, each conflicted path is recorded at three stages instead of the usual single stage:

StageMeaningReference
1Common ancestor (base)The file as it was before either branch touched it
2OursHEAD, the branch you were on
3TheirsMERGE_HEAD, the branch being merged in

You can inspect these directly. git ls-files -u shows the unmerged entries with their stage numbers; git show :2:<path> prints "our" version of a file, and so on:

inspect the three stagesshell
$ git ls-files -u
100644 a13c5d... 1  src/header.js
100644 b7f01a... 2  src/header.js
100644 e2c889... 3  src/header.js

$ git show :1:src/header.js > header.base.js
$ git show :2:src/header.js > header.ours.js
$ git show :3:src/header.js > header.theirs.js

You almost never need to dump the stages by hand for ordinary work. The reason to know they exist is conceptual: when Git's conflict markers don't give you enough context, the base version is right there in stage 1, and any merge tool worth using can show you all three side by side.

The resolution workflow

The resolution workflow is short and the same every time. There is no special "merge mode" in Git beyond the in-progress flag; you just edit files like always, then tell Git you are done.

  1. Identify conflicted files with git status. The list under "Unmerged paths" is the work to do.
  2. Open each file and choose the final content. Keep "ours", keep "theirs", combine both, or write something new. Remove every conflict marker line.
  3. Stage the resolved file with git add <file>. Staging a file with markers still in it is the most common rookie mistake; Git will let you do it.
  4. Finish the merge with git merge --continue. git commit also works; Git knows a merge is in progress and will create the merge commit with the right parents.
  5. If you want out at any point before committing, git merge --abort returns the working tree and index to the pre-merge state.
the canonical resolution loopshell
$ git status                       # see what's unmerged
$ $EDITOR src/header.js            # edit; remove all conflict markers
$ git add src/header.js            # mark as resolved
$ git status                       # confirm no unmerged paths remain
$ git merge --continue             # or: git commit
# Optional escape hatch:
$ git merge --abort                # roll back to the pre-merge state

One detail worth internalizing: git add is what tells Git the conflict is resolved. There is no separate git resolve. Adding a file moves it from stage 1/2/3 back into a normal stage 0, and only when every conflicted path is at stage 0 will Git let you finish the merge.

Three-way view with diff3

Git's default conflict markers show only "ours" and "theirs". Often that is enough, but when both sides edited the same lines starting from non-trivial base content, you really want to see all three. Set merge.conflictStyle to diff3 (or the slightly tighter zdiff3) and Git will include the base version inside the markers:

enable three-way conflict displayshell
$ git config --global merge.conflictStyle zdiff3

The same conflict above now looks like this. The middle block between ||||||| and ======= is the common-ancestor content; it is what both sides started from:

src/header.js (diff3 style)javascript
<<<<<<< HEAD
    <h1 className="page-title">Welcome</h1>
||||||| merged common ancestors
    <h1>Hello</h1>
=======
    <h1 className="hero">Hi there</h1>
>>>>>>> feature/new-button
Recommendation

Turn merge.conflictStyle = zdiff3 on globally and leave it. Once you have read a few conflicts with the base visible, you will not want to go back. It transforms "what should I do?" into "what changed on each side, and which intent should survive?"

Mergetools and editor UIs

Conflict markers are perfectly usable, but a three-pane visual tool is often faster for non-trivial conflicts. git mergetool launches a configured external program on each conflicted file in turn:

configure and run a mergetoolshell
$ git config --global merge.tool vscode
$ git config --global mergetool.vscode.cmd 'code --wait --merge \
    $REMOTE $LOCAL $BASE $MERGED'
$ git mergetool

Common choices include vscode, meld, kdiff3, vimdiff, and JetBrains IDE merge views. Most modern editors also detect conflict markers and show inline "Accept Current / Accept Incoming / Accept Both" buttons without needing git mergetool at all. Either path is fine; pick whichever you can reach without thinking, because conflict resolution is friction you want to remove.

Conflict patterns and strategies

Most conflicts you see in practice fall into a few shapes. The handful below covers nearly all of them, and each has a default playbook:

PatternWhat git status reportsHow to resolve
Same lines, different edits both modified Open the file, pick or combine, git add.
Modified on one side, deleted on the other deleted by us or deleted by them Either git rm <file> to accept the deletion, or git add <file> to keep the modified version.
Same path added on both sides both added Edit to reconcile the two contents, then git add.
Rename versus modify renamed alongside conflict markers Confirm the final name, apply the modification under that name, git add.
Generated / lock files both modified on files like package-lock.json Delete the lock file, re-run the generator, then git add. Don't hand-merge generated content.

Automated resolution with -X ours and -X theirs

Sometimes you genuinely don't want to think. The ort merge strategy (Git's default) takes two options, -X ours and -X theirs, that resolve content conflicts by automatically favoring one side. Non-conflicting changes from the other branch still come through:

auto-resolve by sideshell
# Keep our version on every conflicted hunk
$ git merge -X ours feature/new-button

# Keep their version on every conflicted hunk
$ git merge -X theirs feature/new-button
Watch out

-X theirs on a normal code merge will silently throw away changes you may have wanted to keep. There is no conflict marker, no review prompt, no second chance. Reserve -X ours / -X theirs for cases where one side is known to be authoritative: regenerated artifacts, vendored snapshots, machine-written configuration. Never reach for it on a hot file you only half-understand.

Also note the difference from the unrelated -s ours strategy: that one discards the entire other branch's changes and records the merge as a no-op against your tree. -X ours still merges; -s ours does not.

Habits that reduce conflict pain

The cheapest conflict is the one that never happened. A few habits compound over a project's lifetime:

  • Integrate frequently. Pull or rebase main into your feature branch every day or two. Small, regular merges produce small, regular conflicts; long-lived branches turn into archaeological digs.
  • Keep commits small and focused. A commit that does one thing rebases cleanly. A commit that touches twenty files for three reasons is a conflict generator.
  • Don't reformat the whole file at the start of a change. Every other open PR on that file will now conflict with you, and the conflicts will be in the formatting hunks rather than in real differences of opinion. Land formatting changes as their own commit or PR, ideally when no one else is mid-flight.
  • Communicate before refactoring hot files. If you are about to rename a module that ten branches depend on, a heads-up costs nothing and saves everyone an afternoon.

Common pitfalls

Pitfall 1

Committing conflict markers. Git will happily let you git add a file that still contains <<<<<<< on a line. CI will catch it eventually, but reviewers should not have to. Search every resolved file for the marker characters before staging.

Pitfall 2

Resolving without reading both sides. Picking "ours" because it is on top, or "theirs" because the other person seems more senior, is not resolution; it is gambling. Read the base with zdiff3, understand both intents, then choose.

Pitfall 3

Using -X theirs as a shortcut on a real merge. It will silently delete work. The bug shows up days later when someone notices their feature regressed and there is no conflict in the history to point at.

Pitfall 4

Hand-merging generated files. Conflicts in package-lock.json, yarn.lock, Cargo.lock, or minified bundles are almost never worth resolving by hand. Delete the file, re-run the generator, then git add the regenerated result.

Worked examples

Example 1: A four-step resolution of a content conflict

You ran git merge feature/new-button and Git stopped with a conflict in src/header.js. Walk through the canonical four steps.

Step 1. Confirm the scope of the damage:

step 1shell
$ git status
Unmerged paths:
        both modified:   src/header.js

Step 2. Open src/header.js. With merge.conflictStyle = zdiff3, the disputed hunk shows ours, base, and theirs. Decide that you want the "Welcome" text from ours but the new className="hero" from theirs, and edit the file so it reads:

step 2 - resolved contentjavascript
    <h1 className="hero">Welcome</h1>

Step 3. Mark the file resolved and confirm nothing is still unmerged:

step 3shell
$ git add src/header.js
$ git status
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Step 4. Finish the merge. Git uses the default merge-commit message; edit it if you want to record why you chose what you did:

step 4shell
$ git merge --continue
Example 2: Modified on one side, deleted on the other

Your feature branch modified docs/old-guide.md; meanwhile main deleted the same file. The merge produces a different-shaped conflict:

git status outputshell
$ git status
Unmerged paths:
        deleted by them: docs/old-guide.md

Git is telling you "theirs" (the branch you were on, main) deleted the file, but "ours" still has edits to it. Decide:

  • If the deletion was correct, accept it: git rm docs/old-guide.md.
  • If the edits should win and the file should come back, keep it: git add docs/old-guide.md.

Then git merge --continue. Note how the resolution is one command, not an edit; there is no marker syntax for "this file should exist or not."

Example 3: Aborting cleanly when the merge is more than you want to handle right now

You start a merge, see twenty conflicted files, and realize you would rather rebase your feature branch first to shrink the diff. There is no penalty for backing out:

roll back the mergeshell
$ git merge --abort
$ git status
On branch feature/new-button
nothing to commit, working tree clean

git merge --abort is equivalent to git reset --merge in most cases: the index and working tree go back to where they were before git merge started. Treat conflict resolution as exploratory and use this freely.

Example 4: Reviewing the merge commit before pushing

You finished a complicated resolution and the merge commit is in your local history. Before pushing, look at the combined diff to see what the merge commit actually integrated relative to both parents:

review the merge resultshell
$ git diff --cc HEAD~1 HEAD     # combined diff vs first parent
$ git log -1 --cc                # show the merge with combined diff

The --cc (combined) format only shows hunks that differ from both parents, which is exactly the work the merge commit did. If a line you expected to keep is missing, this is where you will see it. Cheap insurance for fifteen seconds.

Sources & further reading

  • Pro Git, 3.2: Basic Branching and Merging Textbook Scott Chacon and Ben Straub

    Walks through a realistic conflict end to end, including the exact conflict marker format, the git status output for unmerged paths, and the edit / add / commit resolution loop.

  • Pro Git, 7.8: Advanced Merging Textbook Scott Chacon and Ben Straub

    The deep dive: the three index stages, git ls-files -u, merge.conflictStyle = diff3, the -X ours / -X theirs strategy options, and reviewing with git diff --cc.

  • git-merge reference Reference Git project

    Authoritative documentation for --abort, --continue, the strategy options, and the "HOW CONFLICTS ARE PRESENTED" section that defines marker syntax.

  • git-mergetool reference Reference Git project

    How to register and invoke a graphical merge tool. Useful if you want a three-pane visual flow without leaving your terminal.

  • Beginner-friendly walkthrough framed around pull requests, including the deleted-file variant and the exact commands GitHub recommends for the common cases.

  • Merge conflicts Tutorial Atlassian Git Tutorials

    Compact tutorial focused on the practical resolution flow, with a clear visual treatment of when Git can auto-merge and when it cannot.