How to Read a Diff — and Resolve Merge Conflicts Without Losing Work
Unified diffs, hunks, conflict markers, three-way merges, and the habits that stop "resolved" conflicts from silently deleting a colleague’s code.
What a diff actually encodes
A diff is not "the two files" — it is the shortest set of edits that turns one into the other. Unified format (what Git, GitHub and most tools show) groups those edits into hunks, each headed by @@ -a,b +c,d @@: "starting at line a, b lines from the old file; starting at line c, d lines from the new file." Lines prefixed - were removed, + were added, and unprefixed lines are unchanged context shown so you can orient yourself.
The crucial mental model: a "changed" line is just a removal plus an addition that happen to be adjacent. The algorithm has no idea you "edited" a line — it only knows one line vanished and another appeared. That is why a one-character typo fix and a total rewrite of a line look identical in a line-based diff, and why character- or word-level highlighting inside the line is so valuable.
Why the same change can produce different diffs
Diff algorithms (Myers, patience, histogram) minimize edit distance, but "minimal" is not always "readable." Moving a function can show up as a delete here and an add there; re-indenting a block makes every line a change. Three settings decide whether a diff is useful:
- Ignore whitespace — hides indentation churn so you see the logical change. Turn it on for reformatted code, off when whitespace is the bug (Python, YAML, Makefiles).
- Ignore case — useful for prose and SQL, dangerous for code where
Idandidare different things. - Granularity — line diffs for code, word diffs for prose, character diffs for minified output or config strings. A word-level diff of two paragraphs shows the three words that changed; a line diff shows "the whole paragraph changed."
Reading conflict markers without panicking
A merge conflict is Git admitting it cannot choose. Between <<<<<<< HEAD and ======= is your side; between ======= and >>>>>>> branch is theirs. The right resolution is almost never "pick one wholesale" — it is usually both edits combined, in the right order, with the duplicated or contradictory lines reconciled by a human who understands the intent.
<<<<<<< HEAD
const TIMEOUT_MS = 5000; // raised for slow clients
=======
const TIMEOUT_MS = 3000; // lowered to fail fast
>>>>>>> feature/fast-failHere neither number is "correct" — two people had opposing goals. The fix is a conversation, not a keystroke. The most common real-world mistake is resolving by keeping HEAD everywhere because it is the default in many editors, which quietly throws away a teammate’s work and passes review because the diff looks clean.
Three-way merges and why the base matters
Git does not compare your branch to theirs directly; it compares each to the common ancestor (the merge base). A line that differs from base on only one side is taken automatically; a conflict is raised only where both sides changed the same region. This is why rebasing frequently reduces conflicts — it keeps the base close — and why a diff against the *base* (not just against the other branch) tells you who actually changed what.
Habits that keep merges safe
- Diff the *resolved* file against both parents before committing — every resolution should be explainable.
- Run the tests after resolving, not before. A conflict-free merge can still be semantically broken (two branches each add a parameter in a different position).
- Never resolve conflicts in lock files by hand — regenerate them with the package manager.
- For generated or minified files, take one side completely and rebuild; do not try to merge output.
- Keep commits small and focused — the single most effective way to make every future diff readable.
For the formatting side of "why is this whole file marked changed?", see our guide on minify vs beautify — a formatter run before a diff is the difference between a 3-line review and a 300-line one.