What a diff actually says

You read them in every code review — here is what the plus and minus lines promise, and what they don't. ← Workbench

Diffs are everywhere: code reviews, git output, patch files, the "changes" tab on any document history. Reading one feels effortless — red lines go away, green lines arrive. But the format carries a few precise promises, and a lot of things people assume it says that it doesn't. Knowing the difference explains why patches fail to apply, why git sometimes shows enormous diffs for tiny edits, and why two tools can disagree about the same change. You can produce one yourself with the text diff tool here.

An edit script, not a story

A diff is a recipe for turning one text into another using only two operations: delete a line, insert a line. Nothing is ever "modified" — when a line changes, the output shows the old text with a minus and the new text with a plus. That's why every change costs two lines of display, and why the tool computes a recipe that is short: the classic algorithm (Myers, 1986) finds the shortest such script, typically in time proportional to the size of the inputs times the size of the edit.

But "shortest" is a statement about line counts, not about meaning. A diff never records why a line changed, what the author intended, or whether the result still works. It is a mechanical translation plan, and that is all.

Reading a unified diff

The near-universal format is the unified diff. A real example — turning a four-line menu into a five-line one:

--- a/menu.txt
+++ b/menu.txt
@@ -1,4 +1,5 @@
 alpha
 beta
-gamma
+gamma changed
 delta
+epsilon

The two header lines name the old and new file. The @@ line is the hunk header and repays careful reading: -1,4 means "starting at line 1 of the old file, this chunk covers 4 lines", and +1,5 says the corresponding chunk in the new file covers 5. Lines starting with a space are context — unchanged lines shown so your eye can orient and so the patch can be anchored. Two conventions hide in the notation: when a hunk is a single line, the count is dropped (@@ -3 +3 @@), and when one side is empty, the count is 0 and the start points just before the content (-0,0 marks a file that doesn't exist on that side — a creation or deletion).

Lines are the atoms

The format has no sub-line resolution. Change one character in a 200-character line and the diff rewrites the whole line; change indentation across a file and every touched line shows as deleted and re-added, even though nothing meaningful happened. Whitespace is content: a line that ends in an invisible trailing space is a different line from one that doesn't, which is why whitespace-only churn can fill a diff and why most tools offer an "ignore whitespace" switch. Even the final newline is real: a file that simply lost its terminating newline earns its own marker (\ No newline at end of file) — a genuine, if tiny, difference in bytes.

Context is the anchor

Those context lines do more than aid readability — they are how a patch finds where to work. To apply a hunk, the receiving end matches the minus/plus lines plus their surrounding context against the target file. If the file has drifted — someone edited nearby first — the anchor misses. Tools differ in how they cope: the classic patch program will search for a new location (reporting the offset) and, as a last resort, match with a couple of context lines ignored ("fuzz"). git apply is stricter: it demands the context exactly where the header says and refuses otherwise. This is the whole story of "patch does not apply" — the recipe is fine, but the kitchen has changed.

The same change, many diffs

A diff is one minimal recipe, not the minimal recipe. Moving a block of text has no "move" operation — it appears as a deletion in one place and an insertion in another, and where exactly the recipes differ, equivalent outputs can. That's why git offers alternative algorithms (patience, histogram) that produce different, equally valid diffs for the same change — tuned for human readability rather than raw minimality. Rename detection is similar: nothing in the format marks a file as renamed; git infers it by comparing similarity, which is why a rename plus small edits sometimes shows as delete + create.

What a diff cannot tell you

Keep these questions away from the diff — it has no answers:

Why? No intent, no reasoning, no link to the discussion that motivated the change.
Is it correct? A syntactically beautiful patch can break the build; only running the code says.
What changed semantically? Reordering functions, renaming variables, reflowing text — all produce large diffs with little or no behavioral meaning, while a single-character change on a long line can flip a program's behavior and look trivial.
What did the file look like before? Only the context lines you see; a diff is not a snapshot of either file.

The practical habit: read the diff to locate what moved, then read the surrounding code to learn what it means. The diff tells you where to look — it deliberately does not tell you what you're looking at.