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.