On This Page

git diff

git diff answers one question — what changed? — but which two things it compares depends entirely on the arguments you give it. That single detail explains almost every “why is my diff empty?” question.

Git keeps three copies of your project at once: the working tree on disk, the index that stages the next commit, and the commit that HEAD points to. Every form of git diff is a comparison between two of those, or between two commits.

The comparison depends on the arguments

With no arguments, git diff compares your working tree to the index. The index is the staging area: a binary file at .git/index listing the exact content that will go into the next commit.

CommandLeft sideRight sideAnswers
git diffworking treeindexWhat have I changed but not staged?
git diff --cachedindexHEADWhat will the next commit contain?
git diff HEADworking treeHEADWhat has changed since the last commit, staged or not?
git diff A Bcommit Acommit BWhat changed between two commits?

--staged is an exact alias for --cached. Both names describe the same comparison, and Git accepts either.

graph LR
    W[Working tree] -->|git diff| I[Index]
    I -->|git diff --cached| H[HEAD]
    W -->|git diff HEAD| H

Why git diff goes blank after git add

This trips up almost everyone, and it is not a bug. Running git add copies the file’s current content into the index. Once the working tree and the index hold identical bytes, there is nothing left to compare, so git diff correctly prints nothing.

# make a change
printf 'alpha\nbeta\ngamma\n' > a.txt
git add a.txt && git commit -m "one"

# edit it again
printf 'alpha\nBETA\ngamma\ndelta\n' > a.txt

git diff            # shows the edit
git add a.txt
git diff            # prints nothing — the index now matches the file
git diff --cached   # shows the same edit, now waiting to be committed

The change did not disappear. It moved from “unstaged” to “staged”, and each comparison only looks at one boundary.

To see everything since the last commit regardless of staging state, compare against HEAD instead:

git diff HEAD

Anatomy of a hunk

A unified diff has three layers: a file header, a hunk header, and the changed lines.

diff --git a/a.txt b/a.txt
index 85c3040..3cc64f7 100644
--- a/a.txt
+++ b/a.txt
@@ -1,3 +1,4 @@
 alpha
-beta
+BETA
 gamma
+delta
  • diff --git a/a.txt b/a.txt names the file on both sides. The a/ and b/ prefixes exist so patches can be applied with -p1.
  • index 85c3040..3cc64f7 100644 gives the abbreviated blob hashes before and after, plus the file mode.
  • --- and +++ label the old and new versions.
  • @@ -1,3 +1,4 @@ is the hunk header. It reads: starting at line 1, the old side spans 3 lines; starting at line 1, the new side spans 4 lines.
  • A leading - marks a line removed, + a line added, and a space a line kept for context.

Context defaults to three lines on each side. Change it with -U<n>, for example git diff -U1 for tighter output.

Two dots versus three dots

Comparing branches has two syntaxes that look similar but answer different questions.

git diff main..feature    # diff between the two branch tips
git diff main...feature   # diff from the merge base to feature

With a feature branch that has diverged, main..feature includes changes made on main since the split, presented as if they were reversals. main...feature shows only what the feature branch added — which is nearly always what you want when reviewing a pull request.

This was verified against a real repository: the two-dot form listed the files changed on both branches, while the three-dot form listed only the feature branch’s file.

Git compares blobs, not files

Underneath, Git does not diff files. It diffs blobs — the content-addressed objects stored under .git/objects. A tree object maps filenames to blob hashes, and a commit points at a tree.

So git diff A B resolves to: walk the two trees, match path to blob hash, and for every path where the hashes differ, run a line diff on the two blobs.

Two consequences follow from that design:

Renames are inferred, not recorded. Git stores no “this file moved” event. It matches a deletion against an addition and, if the content is similar enough, reports a rename. Detection is on by default in modern Git with a 50% similarity threshold; tune it with -M or -M20%.

Binary files are detected by content. If the first 8,000 bytes contain a NUL byte, Git treats the blob as binary and prints Binary files a/x and b/x differ instead of a line diff. Override that guess with --text.

Picking a diff algorithm

The default is Myers, the classic algorithm from 1986. It produces a short edit script, but on code with moved blocks it can produce confusing hunks.

git diff --diff-algorithm=patience   # better with moved or duplicated blocks
git diff --diff-algorithm=histogram  # often the cleanest for source code
git diff --diff-algorithm=minimal    # slower, tries harder for the shortest diff

patience is worth trying when a refactor reorders functions and the default output looks like a rewrite. Set a permanent default with:

git config --global diff.algorithm histogram

Note that the algorithm changes only how the diff is presented. It never changes what the commits contain.

Flags worth memorising

FlagEffect
--statSummary of files and line counts instead of full hunks
--name-onlyJust the changed paths
--name-statusPaths prefixed with A, M, D, R
-w / --ignore-all-spaceIgnore whitespace-only changes
--word-diffInline word changes using [-old-]{+new+} markers
--color-wordsSame idea, rendered with colour instead of markers
--no-indexDiff two arbitrary paths outside any repository
--stat -MInclude rename detection in the summary

Exit codes for scripts

Plain git diff exits 0 even when it prints differences. That makes it useless as a CI check on its own. Add one of these:

git diff --exit-code    # exit 1 when there are differences
git diff --quiet        # same, but prints nothing

Both were confirmed to return 1 on a dirty tree and 0 on a clean one. Combine with --cached to gate a commit, or with HEAD to gate a build.

When diff output misleads you

Line endings. If core.autocrlf rewrites endings on checkout, every line can appear changed. Normalise with a .gitattributes entry such as * text=auto before blaming the diff.

Generated files. A lockfile diff can bury the change you care about. Exclude paths with a pathspec: git diff -- ':!package-lock.json'.

Large or binary assets. Use --stat first, then narrow to the files that matter.

Textconv filters. Git can run a converter before diffing, which is how git diff shows readable output for Word documents or PDFs. If a file diffs as binary when you expect text, check .gitattributes for an assigned diff= driver.

Recipes

Review exactly what you are about to commit:

git diff --cached

See everything since the last commit, staged or not:

git diff HEAD

Check what a branch adds relative to its merge base:

git diff main...feature --stat

Find whitespace-only noise before a review:

git diff -w --stat

Compare two files with no repository at all:

git diff --no-index old.txt new.txt

FAQ

Why does git diff show nothing after git add?

Because git diff compares the working tree to the index. Staging copied the current content into the index, so the two sides match. Use git diff --cached to see staged changes, or git diff HEAD to see both staged and unstaged.

What is the difference between git diff --cached and git diff HEAD?

--cached compares the index to HEAD, so it shows only what is staged. HEAD compares the working tree to HEAD, so it also includes edits you have not staged yet.

Does git diff change anything?

No. It is read-only. It writes no objects, moves no refs, and does not touch the index.

How do I diff a single file or directory?

Pass a pathspec after --: git diff -- src/app.ts or git diff --cached -- src/. The -- separates revisions from paths so a filename that looks like a branch name is not misread.

Why does a moved file show as delete plus add?

Either rename detection is disabled, or the content changed too much to meet the similarity threshold. Try git diff -M and lower the threshold, for example -M30%.

Test your knowledge

Loading quiz…