Conflicts: When Git Stops and Asks

This chapter walks you through conflict resolution during merges and rebases, including the tools that make it less painful the second time.

Why Conflicts Happen

A conflict happens when two branches change the same region of the same file in incompatible ways. Git can apply both sets of changes automatically when they don't overlap; when they do, it stops and asks you.

Conflicts show up during:

  • git merge (combining two branches)
  • git rebase (replaying commits on a new base)
  • git cherry-pick (applying a specific commit elsewhere)
  • git pull (fetch + merge or rebase)
  • git stash pop (applying stashed changes)
  • git revert (creating an inverse commit)

The mechanics are the same in every case.

What a Conflict Looks Like

Git marks conflicting regions in the file with <<<<<<<, =======, >>>>>>>:

export function login(email, password) {
<<<<<<< HEAD
  if (!email) {
    throw new ValidationError("email required");
  }
=======
  if (!email || !password) {
    throw new Error("missing credentials");
  }
>>>>>>> feature/login
  ...
}
  • Between <<<<<<< and ======= is the current side (where HEAD is).
  • Between ======= and >>>>>>> is the other side (the branch or commit you're merging in).

Edit the file until you have the version you want. Remove the conflict markers:

export function login(email, password) {
  if (!email || !password) {
    throw new ValidationError("missing credentials");
  }
  ...
}

That's a resolution. Here we took the other side's check but kept the current side's exception type.

The diff3 Style

Default git conflict markers show two sides. diff3 style adds a third: the common ancestor.

git config --global merge.conflictStyle diff3

Now conflicts look like:

<<<<<<< HEAD
  if (!email) {
    throw new ValidationError("email required");
  }
||||||| parent
  if (!email) {
    throw new Error("email required");
  }
=======
  if (!email || !password) {
    throw new Error("missing credentials");
  }
>>>>>>> feature/login

Now you can see what both sides changed from. That's often enough to understand the right merge.

Most experienced users leave diff3 on permanently. There's also zdiff3, a slight improvement that removes common lines from both sides. Both are better than the default.

Resolving a Merge Conflict

git merge feature/login
Auto-merging src/auth.ts
CONFLICT (content): Merge conflict in src/auth.ts
Automatic merge failed; fix conflicts and then commit the result.

Check what's going on:

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/auth.ts

Edit the file. Save. Stage. Commit:

# edit src/auth.ts
git add src/auth.ts
git commit       # git pre-fills a merge commit message

If there are multiple files, stage each as you resolve it. git status keeps the list current.

Bailing out:

git merge --abort

Undoes the attempted merge. Your branch is back where it was.

Resolving a Rebase Conflict

Same conflict markers, slightly different flow:

git rebase main
Auto-merging src/auth.ts
CONFLICT (content): Merge conflict in src/auth.ts
error: could not apply 9b2f11a... add login form

Edit the file. Stage. Continue:

# edit src/auth.ts
git add src/auth.ts
git rebase --continue

Git moves to the next commit. If there's another conflict, repeat. If not, the rebase finishes.

Bailing out:

git rebase --abort

Back to exactly where you started.

The Key Difference Between Merge and Rebase Conflicts

A merge conflict is one conflict. You resolve it once, you're done.

A rebase conflict happens once per commit. Rebasing 10 commits against a changed base can involve 10 separate conflict sessions on the same code, because each commit is being replayed in turn.

If the same conflict appears at every step, consider aborting and merging instead.

Useful Tools During Conflicts

Take one side wholesale:

git checkout --ours src/auth.ts        # our side
git checkout --theirs src/auth.ts      # their side

Handy when one side is clearly right. Remember to git add after.

Watch the semantics during a rebase: "ours" and "theirs" are inverted from what you'd expect. During a rebase, "ours" is the branch you're rebasing onto (the new base), and "theirs" is your branch. Read the git status output carefully.

Show the three versions manually:

git show :1:src/auth.ts    # common ancestor
git show :2:src/auth.ts    # ours
git show :3:src/auth.ts    # theirs

Visual merge tool:

git mergetool

Configure a tool you like:

git config --global merge.tool vimdiff     # or "vscode", "kdiff3", "meld", ...

Most editors have a decent built-in conflict UI. Use whatever clicks.

rerere: Remember Resolutions

If the same conflict keeps showing up (typical during long rebases or recurring merges), rerere records how you resolved it and replays your resolution next time.

Turn it on once:

git config --global rerere.enabled true

Now:

git merge feature/login           # conflict; you resolve it
# rerere records your resolution
git merge --abort                 # throw away the merge
git merge feature/login           # conflict reappears
# rerere applies your previous resolution automatically

You still need to git add and commit. rerere handles the editing, not the staging.

During a rebase that hits the same conflict on every commit, rerere is a significant quality-of-life win. Turn it on and forget about it.

Merging Binary Files

Git can't auto-merge binary files (images, PDFs, compiled artifacts). It picks one side or asks you to pick.

git checkout --ours path/to/file.png
git checkout --theirs path/to/file.png
git add path/to/file.png

The better answer is keeping binaries out of git. Use Git LFS (Chapter 11) for large or frequently-changing binaries.

Preventing Conflicts

You can't eliminate conflicts, but you can reduce them:

  • Pull often. Merging a month of divergence is harder than merging a day's.
  • Keep features small. A PR that touches 200 files will conflict with someone.
  • Agree on formatting. A lint/format run before every commit prevents whitespace-driven conflicts.
  • Coordinate on hot files. If two people are rewriting the same module, merge frequently or one of you waits.

Common Pitfalls

Committing with conflict markers still in the file. Git doesn't stop you. The file compiles, then your tests fail mysteriously. Always run the tests before committing a merge.

Confusing "ours" and "theirs" during rebase. The labels swap. Read git status carefully. When unsure, open the file and look at the markers; they're labeled with branch names.

Panicking and rm-ing files. git merge --abort or git rebase --abort is almost always the right escape hatch.

Not turning on diff3 (or zdiff3). Default conflict markers are strictly worse. Turn it on.

rerere recording a bad resolution. If rerere learns the wrong answer, clear it:

git rerere clear

Or forget one specific resolution:

git rerere forget path/to/file

Next Steps

Continue to 08-rewriting-history.md for amend, reset, revert, and the reflog.