How to resolve Git merge conflicts (markers, mergetool, --ours/--theirs, rerere)

· Category: Git

Short answer

When two branches change the same lines, Git stops the merge and writes conflict markers into the affected files. Resolution is a four-step loop:

git status                       # 1. see which files are conflicted
$EDITOR src/api.js                # 2. edit each file: pick a side, splice, or rewrite
git add src/api.js                # 3. mark this file resolved
git commit                       # 4. finish the merge (or `git merge --continue`)

If you want to bail out and try again later, git merge --abort returns the working tree to its pre-merge state.

Reference: git-merge(1) — HOW CONFLICTS ARE PRESENTED.

Reading the conflict markers

By default Git inserts a 3-way diff with two sides:

<<<<<<< HEAD
return user.role === "admin";
=======
return user.role === "admin" || user.role === "owner";
>>>>>>> feature/owner-role
  • Lines between <<<<<<< HEAD and ======= are yours (the branch you were on when you ran git merge).
  • Lines between ======= and >>>>>>> feature/owner-role are theirs (the branch being merged in).
  • The label after >>>>>>> tells you which branch "theirs" came from. During git rebase, the labels swap — HEAD is what's being replayed, so it's actually theirs in the rebase sense. This sign flip is the single most common cause of "I picked the wrong side" mistakes.

For the conflicted view that includes the common ancestor (much more useful when both sides moved a lot), enable diff3 once:

git config --global merge.conflictStyle zdiff3   # Git 2.35+, recommended
# or, for older Git:
git config --global merge.conflictStyle diff3

After this you'll see a third block:

<<<<<<< HEAD
return user.role === "admin";
||||||| common ancestor
return user.isAdmin;
=======
return user.role === "admin" || user.role === "owner";
>>>>>>> feature/owner-role

The middle block is what both of you started from. With it visible, "what did each side actually intend to change" becomes obvious. zdiff3 (added in Git 2.35) is diff3 plus a deduplication step that hides identical leading/trailing context.

Steps for the canonical resolution

# 1. The merge that started it
git switch main
git merge feature/owner-role
# Auto-merging src/api.js
# CONFLICT (content): Merge conflict in src/api.js
# Automatic merge failed; fix conflicts and then commit the result.

# 2. List exactly which paths have unresolved conflicts
git diff --name-only --diff-filter=U
# src/api.js
# tests/api.test.js

# 3. Edit each file. Either remove the markers manually, or use a tool.
$EDITOR src/api.js

# 4. Once a file looks right, mark it resolved
git add src/api.js

# 5. After every conflicted file is staged, finish the merge
git merge --continue          # Git 2.12+; opens the merge commit message editor
# (older equivalent is just `git commit`)

Verify the result before pushing:

git diff HEAD~1 HEAD          # see the actual merge result
git log --merges -1 --stat    # confirm the merge commit looks right

Three resolution tools, in order of usefulness

1. Plain editor + the markers

Any editor with conflict-marker syntax (VS Code, Neovim, JetBrains) highlights the three regions and offers "Accept Current / Incoming / Both". Fine for 80% of conflicts.

2. git mergetool (visual 3-way diff)

git mergetool                # opens whatever's configured (vimdiff, meld, kdiff3, vscode)
# or, force a specific tool:
git mergetool --tool=vscode

Configure once:

git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'
git config --global mergetool.keepBackup false   # don't litter .orig files

A 3-way GUI is dramatically faster than markers when both sides moved a lot of lines.

3. git checkout --ours / --theirs (whole-file shortcut)

When you know an entire file should come from one side:

# Keep the version from the branch you're merging INTO (yours)
git checkout --ours  package-lock.json && git add package-lock.json

# Keep the version from the branch being merged IN (theirs)
git checkout --theirs CHANGELOG.md   && git add CHANGELOG.md

The naming flips during rebase the same way the conflict markers do. The trick: --ours is always "the side you started on when the operation began."

For specific generated files, configure a merge driver instead so you never resolve them by hand:

# .gitattributes
package-lock.json merge=npm-merge-driver

See npm-merge-driver for the canonical example.

Bailing out

If the merge is going badly and you want to start over:

git merge --abort       # restore the pre-merge state
git rebase --abort      # equivalent for an interrupted rebase
git cherry-pick --abort # for cherry-pick conflicts

--abort is safe even mid-resolution — Git unwinds the partial merge and puts you back where you were. The only thing it can't undo is changes you git added and intended to keep.

Avoiding the same conflict twice with rerere

If you rebase a long-lived feature branch repeatedly, you keep resolving the same conflicts in the same way. git rerere ("reuse recorded resolution") records the resolution the first time and replays it next time:

git config --global rerere.enabled true

Once enabled, every conflict you resolve is recorded; the next time the same conflict appears, Git silently applies your previous resolution and you only need to confirm. Doc: git-rerere(1).

Common issues

I committed the conflict markers and pushed. Check with grep -rn '<<<<<<< HEAD' src/ (or your project's source dir). If you find any, the conflict was never resolved. Make a fix-up commit that removes the markers and uses the correct content; don't try to amend if anyone has pulled the broken commit.

git merge --abort says "There is no merge in progress". You ran it after the merge was already committed. Use git reset --hard HEAD~1 to undo the merge commit (only safe if you haven't pushed). For pushed merges, git revert -m 1 <merge-commit> is the right tool — see how to revert a commit in Git.

--ours and --theirs did the opposite of what I expected. You're in git rebase, not git merge. During rebase, HEAD is the commit being replayed onto the new base, so --ours means "the commit being replayed" not "the branch you started on". When in doubt, run git status first — it shows the current operation explicitly.

Binary file conflict. Git can't merge binaries. Pick one side wholesale (git checkout --ours image.png && git add image.png) or restore from somewhere else and git add the file. Generated binaries like node_modules/.cache/*.png should be in .gitignore so they never conflict.

Same conflict over and over during rebase. Either turn on rerere (above) or stop rebasing — there's a structural problem. Either the feature branch has diverged too far from main and should merge instead of rebase, or two engineers are repeatedly editing the same lines and need to agree on a sequence.

Tips

  • git config --global merge.conflictStyle zdiff3 is the single highest-leverage one-liner for conflict UX. Set it on every machine you use Git from.
  • During a merge, git diff (no args) shows only conflicted hunks — use it as your "what's left" indicator alongside git status.
  • git ls-files -u lists every conflicted path with its three stages (1=ancestor, 2=ours, 3=theirs). Useful in scripts or hooks.
  • For preventing painful conflicts, see how to handle merge vs rebase for integrating changes and the related entry on feature branch workflow.
  • IDE integrations: VS Code merge editor docs and JetBrains merge tool docs cover the platform-specific UI.