DiffCanvas

How to read a diff: hunks, context lines and the unified format

6 min readUpdated May 2026

Over-the-shoulder view of a developer reading source code in an editor on a laptop screen

A diff is not a picture of your changes — it is a set of instructions for turning one file into another, written in a format from 1980s Unix that almost every tool still speaks. Once you can decode the two pairs of numbers in a hunk header and know why three unchanged lines sit around every change, most of what looks like noise in a code review turns back into information.

The anatomy of a unified diff

Almost every diff you will meet — the output of git diff, a .patch file attached to a mailing-list message, the text behind the coloured rows in a web code review — is in the unified format standardised as part of POSIX diff -u. It opens with two header lines: one beginning with three hyphens that names the old file, and one beginning with three plus signs that names the new one. The a/ and b/ prefixes git puts on those paths are its own convention for “before” and “after”; plain diff writes the real path and a timestamp instead.

After the headers come the hunks. Each one starts with a line like @@ -18,7 +18,8 @@ and is followed by the lines themselves, one per row, each carrying exactly one prefix character: a space for a line that is unchanged, a minus for a line that exists only in the old file, and a plus for a line that exists only in the new one. There is no marker for “edited”. A changed line shows up as a minus row immediately followed by a plus row, because the format only ever thinks in whole lines.

That single prefix character is why patches survive being pasted into email and why a diff mangled by a chat client stops applying: the first column is data, not decoration. If something strips leading spaces or re-wraps long lines, every context row loses its marker and the patch becomes unreadable to the tools that would have applied it.

Reading the @@ header

@@ -18,7 +18,8 @@ says: this hunk covers seven lines starting at line 18 of the old file, and eight lines starting at line 18 of the new file. The counts include the unchanged context rows, not just the changes, which is why they are almost always larger than the number of plus and minus rows you can see. When a count is exactly 1 it may be left out, so @@ -42 +42,3 @@ is a perfectly legal header.

The two start numbers drift apart as you move down a file. If an earlier hunk added four lines, every later hunk's new-file number sits four higher than its old-file number; that growing gap is a quick way to see the net line change so far. A hunk that only inserts lines has an old count of zero and an old start pointing at the line before the insertion, and a patch that deletes a whole file ends with +0,0.

Git adds a hint after the closing @@ — usually the enclosing function, class or section heading. It finds it by scanning backwards for a line that matches a “hunk header” pattern for the detected language. It is a convenience rather than part of the format, it is sometimes wrong for languages git does not know, and you can improve it by assigning a diff driver in .gitattributes, for example *.py diff=python.

Context lines, and why the default is three

The rows without a plus or a minus are context. A unified diff carries three of them on each side of every change by default, a number inherited from GNU diff. Three is enough for a person to recognise where they are in a file, and enough for the patch program to find the right place again when the target file has drifted by a few lines — without doubling the size of the patch.

Turning context down to zero produces the most compact possible diff, which is what you want when a script is counting changed lines or when a diff is being stored rather than read. Turning it up — git diff -U10, or “all lines” in DiffCanvas — turns the diff into an annotated copy of the file. That is the right view when the change itself is tiny but its surroundings decide whether it is correct, such as a one-character edit inside a long regular expression or a flipped comparison deep in a conditional.

Context also decides how many hunks you get. Two changes four lines apart are two separate hunks with one line of context and a single hunk with three, because their context regions touch and are merged. This is why the same change can be described as “two hunks” by one tool and “one hunk” by another, and why a hunk counter is only meaningful next to the context setting that produced it.

Side by side, inline, and word level

The unified format is a single column. Side-by-side views are a presentation layer on top of it: the tool pairs each removed line with the added line that replaced it and puts the pair in two columns, so you read a change horizontally instead of vertically. It is the better view for edits — a renamed variable, a changed timeout, a reordered argument list — and the worse one for large insertions, where half the screen sits empty while you scroll.

Inline stays closer to the file itself: you read it top to bottom in the order the lines actually occur, which is what you want when a change moves code around or when you are following control flow through the hunk. Most reviewers switch between the two views several times inside a single review, and there is no reason not to.

Word-level marks are a second diff, run inside a pair of lines. The tool first decides which old line corresponds to which new line, then compares those two strings and highlights only the runs that differ. It is only meaningful when the two lines really are versions of each other; when they are not, a careful tool refuses to mark anything and shows a plain removal followed by a plain addition instead of painting half the row. Git has the same idea in git diff --word-diff.

From a view to a patch and back

A diff you can read and a patch you can apply are the same text. Redirect git diff into a file and git apply puts the change back; the older patch -p1 does the same for diffs produced outside git, where -p1 tells it to strip the leading a/ or b/ path component. If the target file has moved on since the diff was made, git apply -3 can fall back to a three-way merge using the blob hashes recorded in the index line git writes above the file headers.

What a web review interface shows you is not a patch, and its buttons change the view without changing the commit. Hiding whitespace-only changes, collapsing generated files, or switching to the split view all happen in the browser; the diff underneath is still whatever git computed. That distinction matters when a reviewer says “there is nothing in this file” and the patch says otherwise.

When you export a patch from DiffCanvas you get exactly that unified text: the context you selected, the file names taken from the files you opened, and hunk headers computed from the line numbers on screen. It pastes into a review, applies with git apply, and can be read by anyone who knows the handful of rules above.

The takeaway

A unified diff has three kinds of row and one header worth decoding. Once @@ -18,7 +18,8 @@ reads as “seven old lines from 18, eight new lines from 18”, and you know that the context setting decides both how much you see and how many hunks you get, a review stops being about the format and goes back to being about the change.

Related tool
Compare two versions

Open the diff tool and paste both versions