Course outline · 0% complete

0/28 lessons0%

Course overview →

git diff

lesson 3-2 · ~12 min · 7/28

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 comparing

The 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:

CommandComparesAnswers
git diffworking directory vs staging area"what have I edited but not staged?"
git diff --stagedstaging area vs last commit"what will my next commit contain?"
git diff a1b2c3d c7d8e9ftwo 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.

diff --git a/soup.txt b/soup.txtindex 8f2a1c4..b7e3d90 100644--- a/soup.txt+++ b/soup.txt@@ -1,4 +1,4 @@Tomato soup- 1 tsp salt+ 2 tsp saltSimmer 20 minuteswhich file, and itsbefore and after idsold version is a,new version is bhunk header: 4 linesfrom line 1, both sidesremoved lineadded lineunlabelled lines arecontext, unchanged
An annotated git diff showing the file header, the hunk range marker, the removed and added lines, and the surrounding context lines.

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.