Conflicts: What They Are, How They Resolve
You can read a conflict, decide what the merged line should say, and commit
Learning Objectives
- Read a conflict's markers and say which side is yours before looking at either
- Resolve a conflict by picking, by combining, and by discarding — on purpose
- Use
git checkout --oursand--theirsknowing which is which during a rebase - List the unresolved files without scrolling a wall of terminal output
The Problem
You pull. Git prints twelve lines of conflict, you scroll past them, and the terminal is full of <<<<<<<, =======, >>>>>>> scattered through three files. You do not know which side is yours, which is the incoming change, or whether git has already written something it wants you to throw away.
So you pick a side. git checkout --ours file — because you remember that command from somewhere. Two of the three files now lose their incoming changes without anyone noticing. The build passes. You commit.
Three weeks later a feature goes missing in production and the git history says you were careful.
A conflict is not a failure of git. It is git declining to guess. The problem was never the conflict — it was resolving it by pattern instead of by reading.
The Concept
A conflict happens when both sides changed the same region and git cannot prove the two intents are compatible. Anywhere git can decide — you edited line 3, they edited line 40 — it merges silently. Conflicts are the residue, not the work.
Git marks the spot in the file itself:
<<<<<<< HEAD: feature
ours = "green"
=======
theirs = "greenish"
>>>>>>> main
Read that carefully, because the words are the trap:
| Word | What it means | Which side |
|---|---|---|
HEAD: | your current branch | ours |
| the other label | the branch you merged in | theirs |
And the trap: git checkout --ours means ours = the branch you are on right now. It does not mean "the change I want". If you are rebasing, "ours" and "theirs" flip, because during a rebase you are conceptually the incoming side. This is the single most common way people silently discard work.
Three ways out
flowchart TD
C[Conflict markers in file] --> D{Is this region a text choice<br/>or a logic choice?}
D -->|Text: two versions of the same line| M[Delete the markers, keep the correct line]
D -->|Both edits are real and belong together| B[Keep both, combine by hand]
D -->|One side is obsolete| O[Take one side deliberately<br/>then verify the surrounding code]
M --> S[git add the file]
B --> S
O --> S
S --> F[git rebase --continue / git commit]A conflict is a decision, not a merge algorithm's leftover work. The three outcomes are: pick, combine, discard — and discarding is legitimate when the other side's change is genuinely obsolete, but you have to decide it.
Merge drivers
Git can be told how to merge certain files without markers — common for package-lock.json, generated code, or structured formats. .gitattributes with merge=<driver> does this. Worth adding when a generated file conflicts constantly, noise. Not worth it while you are still learning to read a real conflict.
Conflicts are cheap when commits are small
The real cost of a conflict is proportional to how much has to be re-read. A twelve-line conflict on a three-commit branch is a minute. A twelve-line conflict between two branches that diverged four hundred commits ago, touching a file with business rules in it, is an afternoon.
git diff --name-only --diff-filter=U # exactly which files are unresolved
Start there. Not by scrolling the terminal.
The reframe: conflict markers are not an error to clear — they are git
asking one question, "which of these two intents do you want?", and the
answer is whatever the file should say, not whatever is easier to delete.
Build It
1. Force a conflict you control
cd ~/git-drill
git switch -c feature
printf 'color = "green"\n' > theme.txt && git add theme.txt && git commit -m "green"
git switch main
printf 'color = "greenish"\n' > theme.txt && git add theme.txt && git commit -m "greenish"
git switch feature
git merge main
You now have a real conflict in a one-line file.
✅ You pass when git status reports an unmerged path and the file contains <<<<<<<, =======, and >>>>>>>.
2. Read the labels before you read the sides
cat theme.txt
Confirm that HEAD is feature and that the bottom half is the commit from main.
✅ You pass when you can state, before looking at which value you prefer, which half is yours and which is incoming.
3. Resolve by hand and commit
printf 'color = "green"\n' > theme.txt
git add theme.txt
git commit # merge commit, conflict resolved
git status
✅ You pass when the merge commit exists and git status reports no unmerged paths.
4. Take a side deliberately
Redo the conflict, then:
git checkout --ours theme.txt # your branch's version
git add theme.txt
git commit
Now do the same with --theirs. Both are legitimate. Neither is a default.
✅ You pass when you have committed one version each way, and can say which flag produced which result.
5. Check what a rebase does to those words
git switch feature
git rebase main
cat theme.txt # look at the labels again mid-conflict
During a rebase, HEAD is the upstream branch you are replaying onto, so the label you expected to be "yours" is not.
✅ You pass when you can point at the labels in the file and say that ours and theirs mean the opposite here, and that this is why git checkout --ours is dangerous during a rebase.
6. Use the unresolved-files list
git diff --name-only --diff-filter=U
✅ You pass when this lists the conflicted files with no manual scrolling, and you start from it next time.
Use It
| Command / concept | When you reach for it |
|---|---|
<<<<<<< HEAD | The top half is your current branch |
======= | Boundary between the two sides |
>>>>>>> | The bottom half is the branch you merged in |
git checkout --ours <f> | Take your current branch's version, deliberately |
git checkout --theirs <f> | Take the incoming version, deliberately |
git diff --name-only --diff-filter=U | Which files are actually unresolved |
git add <f> | Marks a file resolved — until then the merge cannot complete |
git merge --abort | Bail out of an in-progress merge |
| Ours/theirs flip in rebase | Rebase reverses which side is which. Check the labels, not your memory. |
| Merge drivers | Generated files that conflict constantly and never need real resolution |
Exercises
- Produce a conflict deliberately on a file with more than one conflicting region. Resolve each region by a different route: one pick, one combine, one discard. ✅ You pass when the merge completes,
git statusis clean, and you can list which region took which route and why.
- Resolve a conflict the wrong way on purpose — take
--ourson a file where the incoming change mattered — and observe what you lost. Revert it. ✅ You pass when you have the reverted state back and a one-line note on what the--ourshabit costs when used without reading.
- Set up a rebase conflict. Before resolving, write down which side
HEADrefers to in the markers. Then check. ✅ You pass when your written answer matched, and you can say why rebase reverses ours and theirs.
Key Terms
| Term | What people say | What it actually means |
|---|---|---|
| conflict | "git broke my file" | Both sides changed a region and git declined to guess. The markers are the question. |
ours | "my change" | The branch you are on right now. Not "the change I like". |
theirs | "their change" | The branch you merged in. Reversed during a rebase. |
| resolved | "the markers are gone" | The file has been git added. Until then the merge cannot complete. |
| unmerged path | "broken file" | A path git still considers unresolved. git status names them. |
| merge driver | "smart merge" | Configured handling for files that should never need human resolution. |
git merge --abort | "start over" | Returns to the pre-merge state. Only while the merge is in progress. |
What this lesson skipped
Rerere. Git remembers how you resolved a conflict and replays it next time the same one appears. Genuinely useful on long-lived branches. Add it when conflicts start costing you repeatedly, not now.
Resolving submodule and rename/delete conflicts. Both look exactly like a text conflict but are not. Rare, and easier to diagnose once text conflicts are boring.
Manual three-way merges and git merge-file. For when the conflict is in your editor rather than in git's. Not on this path.
Check yourself
4 questions. Answer before you reveal — recognising the right option is not the same as being able to produce it.
-
A rebase stops on a conflict. You want the version that was on your branch before the rebase started. Which command gets it?
Show answer
B. git checkout --theirs <file>, because during a rebase you are the incoming side
During a rebase, HEAD is the branch you are replaying onto, so the side labelled theirs is the one you brought with you. The temptation to reach for
--oursis exactly the trap (A, and C with a skip that also discards work). Both sides are labelled in the file (D) — read the labels at the top of the conflict rather than relying on which words feel right. -
A merge left markers in three files and git printed a wall of output. What is the first command to run?
Show answer
A. git status, then git diff --name-only --diff-filter=U to get the exact unresolved list
Get the list before reading files — the terminal output mixes conflicts with progress noise, and
--diff-filter=Unames exactly the paths git still considers unresolved. Deleting markers file by file (B) is what happens next, but starting from a list means you know when you are done. Bulk-taking theirs (C) silently discards your own work, and--continue(D) is for a rebase in progress and will not resolve anything here. -
Two branches both changed the same three lines of a config file, in different ways, and neither edit is obsolete. What is the correct resolution?
Show answer
C. Combine both edits by hand into what the file should actually say, then git add it
When both edits are real, neither side is the answer — the question is what the file should say, and only you know that. Ours (A) and theirs (B) are preferences dressed as resolutions, and each silently deletes a real change. Rebase (D) applies the same merge machinery and will land on the same conflict; the algorithm was never going to decide this one.
-
A merge conflicts over package-lock.json, which nobody edits by hand and which conflicts every few weeks. What is the actual fix?
Show answer
B. Configure a merge driver for it in .gitattributes so git resolves it without markers
The lesson's test is whether a human ever needs to decide anything about this file. For a generated lock file, the answer is no, so a configured merge driver in .gitattributes is the mechanism for that. Deleting it (A) and ignoring it (C) each trade a real problem for a worse one. Always taking ours (D) is not the same thing — it still produces a conflict every time and still needs a human to pick a side, which is precisely the cost you were trying to remove.