Seeing exactly what changed
A diff is a line-by-line comparison of two versions of a file: which lines were removed, which were added. This idea is older than Git. Linux ships a diff command that compares any two files, and Git's diffing is a descendant of it.
Let's use the plain Linux diff first, since it is the simplest place to start. Its output style takes one minute to learn: lines starting with < are from the first file, lines starting with > are from the second.
Comparing two files with plain diff
An old and a new version of an ingredient list, compared with Linux's own diff.
printf "flour\neggs\nmilk\n" > old.txt printf "flour\neggs\noat milk\nsugar\n" > new.txt diff old.txt new.txt echo "done comparing"
Output
3c3,4
< milk
---
> oat milk
> sugar
done comparingThe notation reads more easily than it looks. 3c3,4 says that line 3 of the old file changed into lines 3 and 4 of the new one, the < line carries the old text, the > lines carry the new text, and the --- divides them.
Notice what is absent. flour and eggs are identical in both files and go unmentioned, because diff reports only differences, and that omission is what keeps a comparison of a thousand-line file readable. The range on the right of 3c3,4 appears because one line became two, milk turning into oat milk and sugar.
git diff's three questions
Git's version compares the three zones from lesson 2-2 and marks removed lines with - and added lines with +:
$ git diff -milk +oat milk +sugar
The command has three everyday forms:
| Command | Compares | Answers |
|---|---|---|
git diff | working directory vs staging area | "what have I edited but not staged?" |
git diff --staged | staging area vs last commit | "what will my next commit contain?" |
git diff a1b2c3d c7d8e9f | two commits | "what changed between these snapshots?" |
A classic moment of confusion: you edit a file, run git add, then git diff prints nothing. Nothing is wrong. The edit moved into the staging area, so working directory and staging area now match. Ask git diff --staged to see it.
Reading Git's real output: hunks
The git diff sample above was trimmed to its minus and plus core. The real output wraps those lines in a little machinery, and it stops looking cryptic once you know why each piece is there.
diff --git a/pancakes.txt b/pancakes.txt --- a/pancakes.txt +++ b/pancakes.txt @@ -3,1 +3,2 @@ -milk +oat milk +sugar
The header names the two versions being compared, where a/ is the old one and b/ the new one. Those prefixes are conventions rather than real directories.
The @@ -3,1 +3,2 @@ line is a hunk header, and a hunk is one changed region of the file. Showing a large file whole would be unreadable, so Git prints only the changed regions plus a few surrounding context lines, which appear with a leading space. The numbers say that this hunk starts at line 3 and covers 1 line in the old version, and starts at line 3 covering 2 lines in the new one.
This format is called the unified diff, and learning to read it pays off well beyond the terminal, since it is exactly what code review tools render in unit 8. Linux's diff produces it too, with the -u flag, which is what the next example uses.
The unified format from plain diff
The same two ingredient lists, this time compared with diff -u. The -u flag asks for the unified format, the one Git prints and the one code review tools render.
printf "flour\neggs\nmilk\n" > old.txt printf "flour\neggs\noat milk\nsugar\n" > new.txt diff -u old.txt new.txt | tail -n +3 echo "done"
Output
@@ -1,3 +1,4 @@ flour eggs -milk +oat milk +sugar done
The tail -n +3 trims the two timestamp lines diff -u puts on top, which would otherwise change on every run. Everything after them is the hunk itself.
Reading the hunk header, the old file's region starts at line 1 and runs 3 lines while the new file's starts at line 1 and runs 4. The file grew by one line, which is what changed the second number.
The first character of every line carries the meaning. Context lines like flour and eggs begin with a single space, removed lines with a minus, and added lines with a plus. That leading space is easy to overlook and is the only thing separating unchanged context from a change. Compare the whole thing with the 3c3,4 output above: same two files, same comparison, different notation.
A one-line change, in diff's default notation
Two three-line files differing in exactly one place, before.txt holding red, green, blue and after.txt holding red, yellow, blue.
printf "red\ngreen\nblue\n" > before.txt printf "red\nyellow\nblue\n" > after.txt diff before.txt after.txt echo "done"
Output
2c2 < green --- > yellow done
2c2 means line 2 changed into line 2. The c stands for changed, and both numbers are 2 because the file neither grew nor shrank. Lines 1 and 3 are identical in both files, so diff says nothing at all about them.
One detail about the echo is worth explaining, since it looks like a stylistic choice and is not. diff exits with a nonzero status whenever the files differ, so chaining with && would have suppressed the echo here. A nonzero exit from diff means the files are different rather than something went wrong, which is a convention shared by several comparison tools and one that trips up shell scripts regularly.
Reviewing what the next commit will contain
After staging with git add, the command that shows what the next commit will contain is git diff --staged. It compares the staging area against the last commit, which is exactly that question.
Two near-misses are worth naming. Plain git diff shows nothing at this point, because the working directory and the staging area became identical the moment you ran add. And git status names the changed files but never shows the changed lines, so it cannot answer a question about content.
When git diff goes quiet
Edit notes.txt, run git add notes.txt, and plain git diff prints nothing at all. The flag that brings the change back into view is --staged (or its older synonym --cached).
Nothing was lost. The change moved out of the working directory and into the staging area, the waiting room from lesson 2-2, and git diff compares the working directory against that waiting room. Those two are identical right after an add, so there is genuinely nothing for it to report. git diff --staged compares the waiting room against the last commit instead, which is where the change now lives.