# Resolving conflicts

> Understand why merge conflicts happen, read conflict markers, and resolve, verify or abort a conflicted merge without panic.

Git & Collaboration roadmap · Everyday workflow · 17 min read · https://levelcode.org/roadmaps/git/merge-conflicts

A merge conflict looks alarming the first time: capital letters, angle brackets in your code, and a repository that seems stuck. In reality a conflict is Git being honest. Two people changed the same thing in different ways, and Git refuses to guess which one is right. This lesson explains exactly when conflicts happen, how to read what Git is showing you, and a calm, repeatable procedure for resolving them — plus how to back out safely when you are not ready.

## Why conflicts happen

Recall that a three-way merge compares each branch tip with the **merge base**, the last commit both branches share. For every file, Git asks: did one side change this region, or both?

| Merge base | Your branch (ours) | Other branch (theirs) | Result |
| --- | --- | --- | --- |
| `USD` | `USD` | `EUR` | Clean: take `EUR` |
| `USD` | `GBP` | `USD` | Clean: take `GBP` |
| `USD` | `GBP` | `GBP` | Clean: both agree |
| `USD` | `GBP` | `EUR` | **Conflict**: both changed it differently |

Only the last row needs a human. Git works line by line, so changes to different parts of the same file merge cleanly; a conflict needs both sides to touch the **same or adjacent lines**. The other common causes are structural: one side edits a file the other deletes (*modify/delete*), or both sides create a file at the same path (*add/add*).

The opposite also matters: a clean merge is not a guarantee of correct code. If you rename a function on one branch and a teammate adds a new call to the old name on another, Git merges both happily and the build breaks. Always run your tests after a merge, conflicted or not.

## Anatomy of a conflict

Here is a real example. Both branches started from this `config.js`:

```js
export const config = {
  title: "My Shop",
  currency: "USD",
  pageSize: 20,
};
```

On `main` you changed the currency to `GBP` and the page size to `50`. On `feature/euro` a teammate changed the currency to `EUR`. Now merge:

```bash
git switch main
git merge feature/euro
```

```text
Auto-merging config.js
CONFLICT (content): Merge conflict in config.js
Automatic merge failed; fix conflicts and then commit the result.
```

The merge is **paused**, not failed. Git merged everything it could, and left you in a special state. `git status` shows what is still open:

```text
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:   config.js
```

Open the file and you will find **conflict markers** around the disputed region:

```text
export const config = {
  title: "My Shop",
<<<<<<< HEAD
  currency: "GBP",
  pageSize: 50,
=======
  currency: "EUR",
  pageSize: 20,
>>>>>>> feature/euro
};
```

- Between `<<<<<<< HEAD` and `=======` is **ours**: the version on the branch you are on.
- Between `=======` and `>>>>>>> feature/euro` is **theirs**: the version from the branch being merged in.
- Everything outside the markers merged cleanly and needs no attention.

Why is `pageSize` inside the conflict, when only you changed it? Because your two changed lines are adjacent and Git treats them as one chunk. Lines you see inside a conflict are not necessarily *disputed*; they are just in the same hunk.

## Resolving step by step

Resolution is always the same four steps.

**1. Understand both intentions.** Do not just pick a side. Ask: what was each change trying to achieve? Here, you wanted bigger pages and a currency switch; your teammate wanted euros. Perhaps after talking to them you agree the shop is moving to euros, but bigger pages should stay.

**2. Edit the file into its final form** and delete every marker line:

```js
export const config = {
  title: "My Shop",
  currency: "EUR",
  pageSize: 50,
};
```

The correct result is often *neither* side exactly but a combination, which is why this needs a person.

**3. Mark the file resolved** by staging it. For Git, `git add` on a conflicted file means "I have fixed this":

```bash
git add config.js
git status
```

```text
On branch main
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:
	modified:   config.js
```

**4. Verify, then conclude the merge.** Make sure no markers slipped through, run the tests, then commit. Git pre-fills the message (`Merge branch 'feature/euro'`); `--no-edit` accepts it.

```bash
git diff --cached --check     # reports any leftover conflict markers
npm test                      # or whatever your project uses
git commit --no-edit
```

```text
*   da2ed46 Merge branch 'feature/euro'
|\  
| * 30f3a16 Switch currency to EUR
* | 3b53ed7 Use GBP and larger pages
|/  
* c154657 Add config
```

If several files conflict, repeat steps 1–3 for each; `git status` lists the ones still unmerged. If you run `git diff --check` *before* staging, it flags markers too:

```text
config.js:3: leftover conflict marker
config.js:6: leftover conflict marker
config.js:9: leftover conflict marker
```

> [!WARNING]
> Committing a file that still contains `<<<<<<<` markers is the classic conflict mistake, and Git will not stop you once the file is staged. Search for the markers or use `git diff --cached --check` before every merge commit.

### Backing out

If the conflict is bigger than you expected, or you need to ask someone, stop cleanly:

```bash
git merge --abort
```

This returns the working tree, index and branch to exactly where they were before `git merge`. Nothing you had committed is at risk, which is why a conflict is never an emergency.

## Getting more context

### Show the base with zdiff3

Two-way markers hide crucial information: what the code looked like **before** either change. Turn on the `zdiff3` conflict style once and every conflict shows the base as a third section:

```bash
git config --global merge.conflictStyle zdiff3
```

```text
<<<<<<< HEAD
  currency: "GBP",
  pageSize: 50,
||||||| c154657
  currency: "USD",
  pageSize: 20,
=======
  currency: "EUR",
  pageSize: 20,
>>>>>>> feature/euro
```

Now it is obvious: the base had `USD` and `20`; you changed both; they changed only the currency. So "their" `pageSize: 20` is not a decision, merely unchanged, and keeping `50` is clearly right. If you are already mid-conflict, `git restore --conflict=zdiff3 config.js` re-creates the markers in this style.

### Inspect the three versions

During a conflict Git keeps all three versions of the file in the index, as numbered **stages**: 1 is the base, 2 is ours, 3 is theirs.

```bash
git show :1:config.js     # merge base version
git show :2:config.js     # ours (HEAD)
git show :3:config.js     # theirs (feature/euro)
git log --merge --oneline # commits on either side that touched conflicted files
```

```text
3b53ed7 Use GBP and larger pages
30f3a16 Switch currency to EUR
```

Reading those commits' messages (`git show 30f3a16`) usually tells you *why* each change was made, which is what you need to resolve correctly.

## Shortcuts and tools

When you know one side should win for an entire file — a generated lockfile, say — take it wholesale:

```bash
git restore --theirs package-lock.json   # or --ours
git add package-lock.json
```

(The older spelling is `git checkout --theirs <file>`.) Be careful: this discards the other side's changes to that file completely, including non-conflicting ones. For lockfiles specifically, it is often better to take one side and then regenerate the file by running your package manager.

Most editors have a merge view. VS Code, for example, shows "Accept Current / Accept Incoming / Accept Both" buttons above each conflict and a three-pane merge editor. `git mergetool` launches whichever external tool you configure. These are conveniences over the same markers; the reasoning in step 1 is still yours.

### Other kinds of conflict

Structural conflicts have no markers to edit. A modify/delete looks like this:

```text
CONFLICT (modify/delete): config.js deleted in feature/rm and modified in HEAD.  Version HEAD of config.js left in tree.
```

`git status` shows `deleted by them: config.js`. Resolve it by choosing: keep the file (`git add config.js`) or accept the deletion (`git rm config.js`), then commit.

You will also meet conflicts outside `git merge` — when pulling, rebasing, cherry-picking or reverting. The markers and the resolve–stage–continue procedure are identical; only the command to finish changes (`git rebase --continue`, `git revert --continue`, and so on), and `git status` always tells you which one.

> [!TIP]
> The best conflict is the one that never happens. Integrate often (merge `main` into long-running work, or keep branches short), keep pull requests small, use an automatic formatter so style changes do not collide, and announce sweeping changes like mass renames before you make them.

## Practice

Create a conflict on purpose and resolve it properly:

1. In a new repository, commit `greeting.txt` containing three lines: `Hello`, `Welcome to the site`, `Goodbye`.
2. On a branch `formal`, change line 2 to `Welcome to our website`. Commit.
3. On `main`, change line 2 to `Welcome to the shop` and line 3 to `See you soon`. Commit.
4. Merge `formal` into `main`. Before resolving, turn on `zdiff3` and regenerate the markers, then use them to work out which side actually changed line 3.
5. Resolve to `Welcome to our shop` / `See you soon`, verify there are no markers, and complete the merge.

**Solution**

```bash
git init conflict-lab && cd conflict-lab
printf 'Hello\nWelcome to the site\nGoodbye\n' > greeting.txt
git add greeting.txt && git commit -m "Add greeting"

git switch -c formal
sed -i 's/Welcome to the site/Welcome to our website/' greeting.txt
git commit -am "Make greeting formal"

git switch main
printf 'Hello\nWelcome to the shop\nSee you soon\n' > greeting.txt
git commit -am "Shop wording and friendlier goodbye"

git merge formal                          # CONFLICT (content) in greeting.txt
git config merge.conflictStyle zdiff3
git restore --conflict=zdiff3 greeting.txt
cat greeting.txt
```

```text
Hello
<<<<<<< ours
Welcome to the shop
See you soon
||||||| base
Welcome to the site
Goodbye
=======
Welcome to our website
Goodbye
>>>>>>> theirs
```

The base section shows `Goodbye`, and "theirs" still has `Goodbye`, so only `main` changed line 3 — keep `See you soon`. Line 2 was changed by both sides; combine them. (When regenerated with `git restore --conflict`, the labels read `ours`, `base` and `theirs`.)

```bash
printf 'Hello\nWelcome to our shop\nSee you soon\n' > greeting.txt
git add greeting.txt
git diff --cached --check     # no output: no markers left
git commit --no-edit
git log --oneline --graph
```

## Key takeaways

- Conflicts arise only when both sides change the same or adjacent lines, or make clashing structural changes such as modify/delete.
- In markers, the `HEAD` section is ours (current branch) and the bottom section is theirs (branch being merged).
- Resolve by understanding both intentions, editing to the final form, `git add` to mark resolved, then committing.
- `merge.conflictStyle zdiff3` shows the merge base, which tells you who actually changed what.
- `git merge --abort` always gets you back to safety; `git diff --check` catches leftover markers.
- A clean merge can still be broken code, so run tests after every merge.

## Quiz

1. During git merge feature while on main, which section of a conflict hunk contains the version from main?

   A. The section between ======= and >>>>>>> feature
   B. The section after |||||||
   C. The section between <<<<<<< HEAD and =======
   D. Neither; main's version is saved to a .orig file

   **Answer: C.** HEAD is the branch you are on, so the top section is ours (main). The bottom section, labelled with the branch name, is theirs. The ||||||| section in diff3/zdiff3 style is the merge base.

2. You edited a conflicted file so that it is correct. How do you tell Git the conflict is resolved?

   A. Run git merge --continue before saving
   B. Stage it with git add, then commit
   C. Delete the .git/MERGE_HEAD file
   D. Run git restore on the file

   **Answer: B.** Staging a previously unmerged path marks it resolved. Once all paths are staged, git commit (or git merge --continue) creates the merge commit.

3. Branch A changed line 10 of utils.js and branch B changed line 80 of the same file. What happens when you merge them?

   A. A conflict, because both branches modified utils.js
   B. Git keeps only the branch you are on
   C. Git asks you to choose a whole-file version
   D. Git merges both changes cleanly

   **Answer: D.** Git merges line by line. Changes to separate regions of the same file combine cleanly; only overlapping or adjacent changes conflict.

4. Halfway through a conflicted merge you realise you need to ask a teammate about their change. What is the safest move?

   A. git merge --abort, then retry the merge once you know the answer
   B. git reset --hard origin/main
   C. Commit the file with markers and fix it later
   D. Delete the conflicted file and commit

   **Answer: A.** git merge --abort restores the exact pre-merge state without touching existing commits. Committing markers breaks the code, and a hard reset to the remote could discard your unpushed commits.
