How to Read a Unified Diff
The unified diff is the format every code review, git diff, and patch file speaks — and once you can read it fluently, reviewing changes gets dramatically faster. The format packs a lot into a few symbols: file headers, hunk headers with line numbers, and single-character prefixes that mark each line as added, removed, or unchanged. This guide decodes every part, line by line, with real examples. Generate and inspect diffs interactively with the diff checker.
A Complete Example, Decoded
Here's a typical git diff for a small change. Every line is labeled below.
diff --git a/greet.py b/greet.py
index 89abf3c..a1b2c3d 100644
--- a/greet.py
+++ b/greet.py
@@ -1,5 +1,6 @@
def greet(name):
- print("Hello, " + name)
+ if not name:
+ name = "world"
+ print(f"Hello, {name}")
greet("Janeer")
Reading it top to bottom:
diff --git a/greet.py b/greet.py— git's header naming the file on both sides.index 89abf3c..a1b2c3d 100644— the old and new blob hashes and the file mode (100644 = a normal non-executable file). You rarely need this line.--- a/greet.py— the old version of the file.+++ b/greet.py— the new version.@@ -1,5 +1,6 @@— the hunk header: old file lines 1-5 (5 lines), new file lines 1-6 (6 lines). Net +1 line.def greet(name):— leading space = unchanged context line.- print("Hello, " + name)— minus = this line was removed.+ if not name:and the two lines after it — plus = added lines.greet("Janeer")— another unchanged context line.
The change replaced one print line with four lines (a guard clause plus an f-string version). Notice the edit is expressed as one removal and three additions — there's no dedicated "this line changed" marker.
The Three Line Prefixes
Inside a hunk, the very first character of every line is metadata, not file content:
-(minus) — present in the old file, removed in the new.+(plus) — added in the new file, absent from the old.- ` ` (space) — unchanged; shown as context so you can locate the change. By default diffs include 3 context lines above and below each change.
A modified line shows as a paired removal and addition:
- color: red;
+ color: blue;
To recover the original file, take every space-and-minus line and drop the prefix. To get the new file, take every space-and-plus line. The space lines appear in both — that's what makes a patch applicable: the context lets the tool find the right spot even if surrounding line numbers have shifted.
The Hunk Header in Detail
@@ -12,7 +12,9 @@ def process(items):
│ │ │ │ │
│ │ │ │ └─ new file: this hunk spans 9 lines
│ │ │ └──── new file: hunk starts at line 12
│ │ └─────── old file: this hunk spans 7 lines
│ └────────── old file: hunk starts at line 12
└───────────── hunk markers
└─ optional: enclosing function/section
The -12,7 describes the old file's slice; +12,9 describes the new file's. When a count is 1, some tools omit it (@@ -5 +5,2 @@ means one old line at line 5, two new lines starting at line 5). The text after the closing @@ is the "section heading" — git tries to show the enclosing function or class to orient you, but it's purely a navigation hint, not part of the change.
Special Cases
New file
diff --git a/new.txt b/new.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/new.txt
@@ -0,0 +1,3 @@
+line one
+line two
+line three
The old side is /dev/null and the old range is -0,0 (zero lines) — the file didn't exist before. Every line is an addition.
Deleted file
diff --git a/old.txt b/old.txt
deleted file mode 100644
--- a/old.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-gone line one
-gone line two
Mirror image: the new side is /dev/null, the new range is +0,0, every line is a removal.
File mode change
A line like old mode 100644 / new mode 100755 indicates a permission change (here, making a file executable) — the content may be identical with no hunks at all.
Binary files
Diffs can't show binary content meaningfully, so you'll see Binary files a/logo.png and b/logo.png differ instead of hunks. Git can produce binary patches with --binary, but they're not human-readable.
"No newline at end of file"
The marker \ No newline at end of file appears when a line isn't terminated by a newline. It's informational — it tells the patch tool the exact byte content so applying the patch reproduces the file precisely.
Creating and Applying Diffs
# Create a unified diff between two files
diff -u original.txt modified.txt > changes.diff
# Git's version of the working-tree changes
git diff > changes.diff
git diff --staged # what's staged for commit
git diff main..feature # between two branches
# Apply a diff
patch -p1 < changes.diff # classic tool; -p1 strips a/ b/ prefixes
git apply changes.diff # git-aware; handles modes and renames
git apply --check x.diff # dry-run: will it apply cleanly?
# Apply a patch that carries commit metadata (from git format-patch)
git am < feature.patch
The -p number in patch -pN is how many leading path segments to strip. Git diffs prefix paths with a/ and b/, so -p1 removes that one segment to get the real path. A plain diff -u between two files usually needs -p0 (strip nothing).
Try It Live
The diff checker compares two blocks of text in your browser and highlights additions and removals visually — a fast way to see what a change contains without reading raw diff syntax. To generate unified diffs in code, see the JavaScript and Python diff guides, both of which produce exactly the format decoded here.
Frequently Asked Questions
What do the @@ symbols mean in a diff?
The line that starts and ends with @@ is the hunk header — it marks the start of a block of changes and tells you where in each file the block sits. The format is @@ -oldStart,oldCount +newStart,newCount @@. The part with the minus refers to the original file: starting line number and how many lines the hunk spans. The plus part refers to the new file. So @@ -12,7 +12,9 @@ means "starting at line 12, this hunk covers 7 lines of the old file and 9 lines of the new file" — a net addition of 2 lines. Some tools append the enclosing function or section name after the closing @@ as a navigation aid.
What do the + and - signs mean at the start of diff lines?
Each line in a hunk has a single-character prefix. A - (minus) means the line was removed from the original file. A + (plus) means the line was added in the new file. A space (no symbol) means the line is unchanged and is shown only as context to help you locate the change. A modified line appears as a - for the old version immediately followed by a + for the new version — diffs have no "changed" marker, every edit is modeled as a removal plus an addition. The leading character is not part of the file content; it's metadata.
How do I apply a unified diff as a patch?
Two common tools. The classic patch command: patch -p1 < changes.diff applies the diff to the files in the current directory — the -p1 strips the leading path component (the a/ and b/ prefixes git adds). For diffs produced by git, git apply changes.diff is usually cleaner because it understands git's path conventions and can handle file modes and renames. To apply and create a commit in one step, git am works on patches that include commit metadata (from git format-patch). If a patch doesn't apply cleanly because the target has drifted, both tools report rejected hunks you then resolve by hand.
What is the difference between unified diff and context diff?
Both show changes with surrounding context, but unified diff is more compact and is the modern default. Context diff (the older format, produced by diff -c) shows the before and after blocks separately, each with its own full set of context lines, marked with *** and ---. Unified diff (diff -u) interleaves removals and additions in a single block with -/+ prefixes and shares the context lines between them, which is roughly half the size and easier to read. Git uses unified diff exclusively. You'll rarely see context diff today outside legacy tooling.
What do the --- and +++ lines at the top of a diff mean?
They are the file header, identifying which two files are being compared. The --- line names the original (old) file and the +++ line names the new file. In git diffs these show as --- a/path/to/file and +++ b/path/to/file, where a/ is the old version and b/ is the new — the prefixes are why you pass -p1 to patch to strip them. A --- path of /dev/null means the file is newly created (nothing on the old side); a +++ path of /dev/null means the file was deleted. The header may also include timestamps or git index lines above it.