---
title: "Merge Conflicts"
description: "Resolve competing edits, stage the result, and finish the interrupted merge."
source: https://vizlearn.app/cs/version-control/merge-conflicts
---

# Merge Conflicts

A merge conflict can look fixed in your editor while Git still refuses to continue. The reason is simple and easy to miss: Git decides whether a path is resolved from the index, not from the presence or absence of conflict markers in the working file. Markers are text; index stages are state.

Think of the working tree as the draft on your desk and the index as the tray that will be filed. Editing the draft does not replace what is in the tray. `git add` does. That small boundary explains both common failures: a clean-looking file that Git still calls unmerged, and a marker-bearing file that Git will happily stage.

## How merge conflicts work

A diverged merge starts with three immutable snapshots:

- **Base** is the shared earlier commit.
- **Ours** is the snapshot at `HEAD`, the checked-out side.
- **Theirs** is the snapshot at the source branch named by `git merge`.

The animation uses a deliberately narrow aligned-line model: one file, three substitution-only lines, and one possible conflict hunk. For each aligned line it applies four rules. If ours equals theirs, take either value. If ours equals base, only theirs changed, so take theirs. If theirs equals base, only ours changed, so take ours. If all three differ, none of those rules can justify a choice. That line conflicts.

The first line makes the equality rule do real work. The base says `title=VizLab`; both branches independently say `title=VizLearn`. Git takes `VizLearn` because ours equals theirs, not because either side matches the base and not because `HEAD` gets a ceremonial tie-breaker.

The conflict does not create a half-finished commit. `HEAD` and the current branch remain on the pre-merge commit. `MERGE_HEAD` records the source tip. For the unmerged path, the index removes stage 0 and stores three entries: stage 1 for base, stage 2 for ours, and stage 3 for theirs. The working file receives a merge result with a marker block such as:

```text
title=VizLearn
<<<<<<< HEAD
accent=navy
=======
accent=teal
>>>>>>> feature
footer=v1
```

You now edit the working file. Git does not infer resolution from appearance, so deleting the markers is not enough. `git add app.txt` hashes the exact working bytes, removes stages 1/2/3, and writes one stage-0 entry. Only then can `git merge --continue` write the result tree, create a commit with two ordered parents, advance the current branch, and clear `MERGE_HEAD`.

There is a sharp edge here: `git add` stages bytes, not good judgment. If the marker block is still present, Git can stage it and the merge commit can preserve it as ordinary file content.

## Merge conflicts step by step

The default trace uses one three-line file and these ten operations:

```text
commit base:          title=VizLab   | accent=blue   | footer=v1
git branch feature
commit main-theme:    title=VizLearn | accent=navy   | footer=v1
git switch feature
commit feature-theme: title=VizLearn | accent=teal   | footer=v1
git switch main
git merge feature
edit app.txt:         title=VizLearn | accent=indigo | footer=v1
git add app.txt
git merge --continue
```

1. The `base` commit records `title=VizLab` and `accent=blue`. Both `main` and `feature` initially point to it.
2. `main-theme` changes the title to `VizLearn` and line 2 to `accent=navy`. After switching to `feature`, `feature-theme` makes the same title change but chooses `accent=teal`. The two tips now share a base but neither contains the other.
3. Back on `main`, `git merge feature` freezes ours at the `main-theme` tip and theirs at the `feature-theme` tip. The unique merge base is `base`. `MERGE_HEAD` points to the feature tip; `HEAD` does not move.
4. Line 1 is the non-trivial equality case: base is `title=VizLab`, while ours and theirs both equal `title=VizLearn`, so Git takes either side. Line 3 is unchanged everywhere. Line 2 is `accent=blue` at base, `accent=navy` in ours, and `accent=teal` in theirs. All three differ, so the engine records one conflict hunk.
5. The index becomes stages 1/2/3 for `app.txt`, and the working file expands to the seven-line marker form shown above. Commit objects added: zero. Current-ref writes: zero.
6. The editor replaces the marker block with `accent=indigo`. Only the working tree changes. The index still contains stages 1/2/3, so the path is still unmerged in Git's authoritative state.
7. `git add app.txt` hashes the three resolved lines and writes that blob to stage 0. The three unmerged stages disappear, `MERGE_HEAD` remains set, and no branch moves.
8. `git merge --continue` verifies that no unmerged paths remain. It writes a tree from stage 0, then creates one merge commit whose first parent is the pre-merge `main` tip and whose second parent is the frozen `feature` tip.
9. Finally, `main` advances to the new merge commit. `feature` stays where it was, `MERGE_HEAD` clears, and the index plus working tree retain the resolved `accent=indigo` snapshot.

In the visualizer, the decisive handoff is the arrow between the two lower cards. Conflict materialization flows from the three-stage index to the working tree. `git add app.txt` reverses the direction and copies the edited bytes back into stage 0.

## Why the index has three stages

Stage numbers are part of the index key. During a normal checkout, `app.txt` has one stage-0 entry. During a content conflict, one path needs three simultaneous blob identities:

| Index stage | Role     | Meaning in this merge                    |
| ----------- | -------- | ---------------------------------------- |
| 1           | base     | The common ancestor's file               |
| 2           | ours     | The file from pre-merge `HEAD`           |
| 3           | theirs   | The file from `MERGE_HEAD`               |
| 0           | resolved | The single version selected by `git add` |

This is better than storing only a red "conflicted" flag. The three blobs preserve the evidence needed by merge tools, while the working file remains an editable proposal. Once stage 0 replaces the unmerged entries, Git knows which exact bytes would enter the next tree. It does not claim those bytes are sensible.

For the mini-engine, finding the merge base costs `O(V + E)` over the reachable commit graph. The aligned content pass costs `O(L + B)` for `L` logical lines and `B` bytes. Hashing the resolution and serializing its tree and commit are linear in the staged bytes. Those bounds describe this scoped engine, not the performance of Git's production `ort` strategy.

## A practical merge-conflict workflow

1. Run `git status` first. Its "unmerged paths" section tells you which paths Git still considers unresolved; the marker text is not the authority.
2. If you want to see the three stored inputs directly, run `git ls-files -u`. Stage 1 is base, stage 2 is ours, and stage 3 is theirs.
3. Edit the file, decide what the program should actually do, and run the relevant tests. A marker-free file is a candidate resolution, not yet a staged one.
4. Run `git add app.txt`, then inspect the staged result with `git diff --cached`. At this point the path has one stage-0 entry instead of stages 1/2/3.
5. Use `git merge --continue` when the staged snapshot is correct, or `git merge --abort` when you want to return to the pre-merge state covered by this lesson.

That order is worth memorizing: inspect state, edit and verify, stage, inspect the staged result, then continue or abort. It turns a mysterious red warning into a sequence of explicit state changes.

## Edge cases that reveal the index boundary

- **Different lines:** if `main` changes line 2 and `feature` changes line 3, the same rules choose both edits cleanly. No stages 1/2/3 and no marker block appear.
- **Continue too soon:** editing the file without `git add` leaves the index unmerged. `git merge --continue` creates no commit and moves no ref.
- **Stage markers:** running `git add app.txt` while markers remain creates a valid stage-0 blob containing those markers. Continuing can commit them.
- **Abort:** in this lesson's clean pre-merge-worktree scope, `git merge --abort` restores the index and working file from the merge-start `HEAD`, clears temporary merge state, and leaves both branch refs fixed.
- **No active merge:** `git merge --continue` and `git merge --abort` fail when `MERGE_HEAD` is absent. The lesson-scoped editor and add actions are unavailable outside a paused merge.
- **Visualizer capacity:** eight commits, three branches, and twenty-four commands are layout limits. Real Git has no such small caps.

## Common mistakes that confuse bytes with resolution

- **Treating conflict markers as the conflict itself.** The authoritative conflict is the path's stages 1/2/3 in the index. Markers are working-tree text produced for a content merge.
- **Assuming an editor resolves Git state.** Saving a clean-looking file changes the working tree only. Run `git add` to replace the unmerged index entries.
- **Believing `git add` validates the resolution.** It stages the current bytes, including accidental marker text, debug output, or a bad combination of both sides.
- **Expecting a conflict commit to appear immediately.** The merge pauses before the result tree, merge commit, and current-ref write.
- **Moving both branches in your mental model.** A completed merge advances the checked-out branch. The source branch remains at its original tip.
- **Forgetting parent order.** Parent 1 is the pre-merge current tip; parent 2 is `MERGE_HEAD`. Those ordered headers are part of the commit bytes and therefore part of its object ID.

## A note on simplification: one file, one hunk

This animation models one clean local repository with attached `HEAD`, one `app.txt` file, exactly three initial logical lines, aligned substitutions, one unique merge base, and at most one conflict hunk. It omits additions, deletions, renames, directory/file conflicts, binary files, multiple merge bases, custom merge drivers, attributes, rerere, hooks, signatures, submodules, sparse or shallow history, pre-existing working-tree edits, and remotes. Real `ort` also writes an `AUTO_MERGE` tree during a conflicted merge; the lesson names that omission instead of drawing a fake approximation.

For production behavior, see [`git merge`](https://git-scm.com/docs/git-merge), [`git ls-files`](https://git-scm.com/docs/git-ls-files), and the [Git Book's conflict walkthrough](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging.html).
