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.
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 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:
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:
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:
| Stage | Meaning | Reference |
|---|---|---|
| 1 | Common ancestor (base) | The file as it was before either branch touched it |
| 2 | Ours | HEAD, the branch you were on |
| 3 | Theirs | MERGE_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:
$ 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.
- Identify conflicted files with
git status. The list under "Unmerged paths" is the work to do. - Open each file and choose the final content. Keep "ours", keep "theirs", combine both, or write something new. Remove every conflict marker line.
- 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. - Finish the merge with
git merge --continue.git commitalso works; Git knows a merge is in progress and will create the merge commit with the right parents. - If you want out at any point before committing,
git merge --abortreturns the working tree and index to the pre-merge state.
$ 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:
$ 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:
<<<<<<< HEAD
<h1 className="page-title">Welcome</h1>
||||||| merged common ancestors
<h1>Hello</h1>
=======
<h1 className="hero">Hi there</h1>
>>>>>>> feature/new-button
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:
$ 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:
| Pattern | What git status reports | How 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:
# 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
-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
maininto 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
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.
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.
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.
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:
$ 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:
<h1 className="hero">Welcome</h1>
Step 3. Mark the file resolved and confirm nothing is still unmerged:
$ 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:
$ 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
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:
$ 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:
$ 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
-
Walks through a realistic conflict end to end, including the exact conflict marker format, the
git statusoutput for unmerged paths, and the edit / add / commit resolution loop. -
The deep dive: the three index stages,
git ls-files -u,merge.conflictStyle = diff3, the-X ours/-X theirsstrategy options, and reviewing withgit diff --cc. -
Authoritative documentation for
--abort,--continue, the strategy options, and the "HOW CONFLICTS ARE PRESENTED" section that defines marker syntax. -
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.
-
Compact tutorial focused on the practical resolution flow, with a clear visual treatment of when Git can auto-merge and when it cannot.