ReadbackCourseReference · freeSign in
Reference7 sections · built in Lesson 08

CONFLICTS,BY KIND

What to run before you open a file, how to tell the three kinds apart from two letters, and what the index is holding while a merge is stopped.

The first four commands

In order, before opening an editor. Each one answers a question the next one depends on.

  1. git status --short — every unmerged path, with its kind in two letters. The file that needs the most thought is often the one that looks untouched.
  2. git log --oneline -1 HEAD and git log --oneline -1 MERGE_HEAD — which commit is on each side, so ours and theirs stop being words you reason about. During a rebase the second one is REBASE_HEAD, and the sides are inverted.
  3. git show :1:<path> — the base. What both sides were changing from, which is what tells you whether two changes are incompatible or merely adjacent.
  4. git ls-files -u <path> — the stage table, when the kind is not obvious. A missing stage is the answer, and no porcelain command shows an absence.

The three kinds

What a conflict actually is

Not “markers appeared in a file”. A conflict is a path git refused to resolve. Two of the three kinds below put no markers anywhere — the file sits in your working tree looking perfectly normal, and it is unmerged.

Kind, as git names itWhat it is, and what it needs from you
CONFLICT (content)Both sides changed the same region relative to the base. Markers in the file. Usually resolved with lines from both sides — which is neither --ours nor --theirs.
CONFLICT (modify/delete)One side changed the file, the other removed it. No markers. This is a question about intent, not about lines: is the file still needed?
CONFLICT (add/add)Both sides created the same path independently. Markers, but no base section — there is nothing to three-way merge against. A decision about which implementation survives.

The dangerous one is modify/delete. Git refuses the commit while the path is unmerged, so it is caught — but the obvious fix, git add <path>, silently keeps your version and discards the other side’s deletion. Nothing warns you.

Reading `git status --short`

Normally the two columns mean index and working tree. For an unmerged path they do not: git-status keeps a second table in which the first letter is ours and the second is theirs (git-status). Applying the ordinary reading to UD gets you the wrong side.

CodeMeaning while unmerged
UUBoth modified — the ordinary content conflict.
AABoth added — the add/add case, no base.
UDWe modified, they deleted. Stages 1 and 2, no stage 3.
DUWe deleted, they modified. Stages 1 and 3, no stage 2.
AUAdded by us — they have no version at all.
UAAdded by them — we have no version at all.
DDBoth deleted. Rare, and resolved by agreeing it is gone.

What the index is holding

Outside a conflict the index holds one entry per path. During one it holds up to three: “stage 1 stores the version from the common ancestor, stage 2 from HEAD, and stage 3 from MERGE_HEAD” (git-merge). The stage column is a general property of the index rather than a merge invention (git-read-tree).

100644 0ef3a021… 1 config.ymlstage 1 — the merge base
100644 209b4a6e… 2 config.ymlstage 2 — ours, i.e. HEAD
100644 a1bc9402… 3 config.ymlstage 3 — theirs
100644 f2dc4e9c… 1 legacy.txtbase exists…
100644 3ea82779… 2 legacy.txt…ours exists, theirs does not → modify/delete
100644 5d6e9041… 2 retry.tsno stage 1 at all…
100644 179e71aa… 3 retry.ts…both sides created it → add/add
git ls-files -u on a merge with all three kinds in it. Count the stages per path: the missing one names the kind. A missing stage is not a gap in the table, it is a fact about the graph — retry.ts has no stage 1 because the file does not exist at the merge base.
Read a stage backGives you
git show :1:<path>The base — what both sides started from. The most useful and least used of the three.
git show :2:<path>ours: HEAD’s version. During a rebase this is the branch you are replaying onto, not your work.
git show :3:<path>theirs: MERGE_HEAD’s version, or the commit being replayed during a rebase.
git diff --base <path>Both sides against the base at once, when reading three files separately gets confusing.

The markers, with the base shown

The default marker style hides the base, which is the piece that decides most resolutions. zdiff3 prints it: the alternate styles add “a ||||||| marker and the original text before the ======= marker” (git-config). Turn it on once — git config --global merge.conflictStyle zdiff3 — and every conflict you meet afterwards tells you more.

<<<<<<< oursHEAD — the branch you are on
timeout: 30000we changed this line
retries: 3we left this one alone
||||||| basethe merge base — only with diff3/zdiff3
timeout: 5000what both sides started from
retries: 3
=======
timeout: 5000they left this one alone
retries: 5they changed this line
>>>>>>> theirsMERGE_HEAD, or the replayed commit
Compared against the base, ours changed only line 1 and theirs changed only line 2. Nothing is genuinely in dispute — git conflicts because the two changed regions are adjacent, and it will not guess at how nearby edits combine. The correct resolution takes one line from each.

To bring the markers back after you have mangled a file: git checkout --conflict=zdiff3 <path> restores the conflicted state from the index, which is still holding all three stages.

git checkout --ours <path> and --theirs <path> take an entire file from one stage. Right for genuinely binary decisions — a lockfile, a generated bundle, a minified asset — and wrong for source, where the answer usually needs lines from both.

Resolving the same conflict twice

rerere — reuse recorded resolution — replays a resolution you have already given for a conflict of the identical shape. It is off by default: git config --global rerere.enabled true.

CommandWhat it does
git rerere statusPaths whose resolution it will record. Content conflicts only — a deletion has no text resolution to remember.
git rerere diffWhat your resolution changed, against the recorded preimage.
git rerere forget <path>Throws away a resolution you recorded wrongly. Documented in the man page and nowhere in the book.
git rerere remainingPaths still unresolved after the replay.

Two properties matter more than the commands, and both are easy to get backwards.

It does not stage anything. The docs are explicit: rerere “leaves the index file alone, so you still need to do the final sanity checks with git diff (or git diff -c) and git add when you are satisfied” (git-rerere). git status still says UU after a replay. That is the safety property — a remembered resolution is a draft, not a decision.

It learns on git commit, not on git add. The preimage is written when the conflict happens; the postimage — your actual answer — appears only when you commit, which is also when the Recorded resolution lines print. So a conflict you resolve, stage, and then git merge --abort teaches it nothing. Established by watching .git/rr-cache/<id>/ before and after; the man page hints at it and leaves the consequence to the reader.

And it is yours alone. .git/rr-cache is a local directory: it is not pushed, and a fresh clone does not have it. A teammate who hits the same conflict resolves it by hand.

Getting out

CommandEffect
git merge --abortBack to before the merge, cleanly, at any point. Nothing is lost.
git rebase --abortThe same for a rebase, however many commits in.
git rebase --skipDrops the commit being replayed and continues. Occasionally right, and worth being sure about.
git reset --mergeAbandons the merge but keeps unrelated local changes that were there before it started.

Knowing these exist is what makes it safe to explore a conflict rather than guess at it.

A4 or Letter
one ink, no chrome
Sources