How to Use git diff for Staged Unstaged and Between Branches
Read what changed with git diff: working tree vs index, staged changes, branch vs branch, the two and three dot forms, and options that cut noise.

Most confusion with git diff comes from not knowing which two things it compares. Git has three places a file can live: the working tree, the index (the staging area) and the last commit. Each form of the command compares a pair. The staging area is explained in the staging area post, and examples use Git 2.47.
Which two things
git diff # working tree vs index: not yet staged
git diff --staged # index vs HEAD: what a commit would record
git diff HEAD # working tree vs HEAD: everything uncommittedA common surprise: you run git add and then git diff shows nothing. The changes moved from "unstaged" to "staged", so look with --staged. --cached is a synonym for it. A file with both staged and later unstaged edits appears in both outputs.
Between commits and branches
git diff main feature
git diff main..feature
git diff main...feature
git diff v2.3.0 v2.4.0 -- src/The first two are identical: compare the two tips. The three-dot form is different, and it is usually what you want for a pull request review: it shows the changes on feature since it forked from main, hiding anything main gained afterward. Add a path after -- to restrict the output.
Summaries and noise reduction
git diff --stat main...feature
git diff --name-status main...feature
git diff -w --ignore-blank-lines
git diff --word-diff=color README.md
git diff --check src/cache.py | 14 +++++++++-----
src/config.py | 3 ++-
2 files changed, 11 insertions(+), 6 deletions(-)
--stat gives the shape of a change before you read it. --name-status adds a letter per file: A added, M modified, D deleted, R renamed. -w hides whitespace-only changes after a reindent. --check warns about trailing whitespace and leftover conflict markers, which makes it a good pre-commit step.
Diffs for scripts
git diff --quiet || echo "uncommitted changes"
git diff --exit-code main...feature >/dev/null; echo $?
git diff -z --name-only--quiet and --exit-code set the exit status to 1 when there are differences, which scripts use to decide whether to commit or deploy. -z separates names with NUL so unusual file names survive. Remember that git diff ignores untracked files; check them with git status --porcelain.
Where this does not apply
For two arbitrary files outside a repository, git diff --no-index a b works, or use plain diff. Large binary files show only "Binary files differ". If you want to review changes interactively, git add -p walks through the hunks. For a commit's own changes, git show is shorter.
One last tip: set diff.algorithm histogram in your configuration. It produces clearer diffs than the default on code that moves blocks around, at no cost. For colored word-level review in the terminal, diff.colorMoved zebra highlights moved lines in a different shade from added and deleted ones.
What each form of diff compares
The same command compares different things depending on its arguments, and most confusion comes from that. With no arguments, git diff compares the working tree to the index, so it shows unstaged changes only. A change already added with git add vanishes from this output, which leads people to think it was lost. git diff --staged, also spelled --cached, compares the index to the last commit, and shows what would go into the next commit. git diff HEAD compares the working tree to the last commit, so it shows both staged and unstaged work together. Keep those three in mind and the output stops surprising you. Untracked files appear in none of them, since Git has nothing to compare, so a new file shows up only after git add or with --intent-to-add.
With two commits, git diff A B compares those two trees directly. With two dots or three, the meaning shifts for diff compared to log, where the symmetric difference is different from what diff does with the same notation.
Branch comparisons and the three dot form
git diff main..feature is the same as git diff main feature, a direct comparison of the two tips. That includes changes made on main since the branch split, shown in reverse, which is rarely what you want when asking what the feature adds. git diff main...feature with three dots compares feature to the merge base of the two, so it shows only the changes made on the feature branch, which is what a pull request displays. The distinction matters in review scripts: choose the three dot form to see what a branch contributes, and the two dot form to see how two tips differ right now. Remember the base moves as main moves, so rerun the comparison before relying on an old result.
Renames are detected by similarity, and the threshold can be tuned with -M followed by a percentage. A file that was both moved and heavily edited may show as a delete and an add if the similarity falls below the threshold.
Reducing noise and getting machine output
--stat shows a summary of files and line counts, and --shortstat gives one line. --name-only and --name-status list files, the latter with a letter for added, modified, deleted or renamed. For whitespace noise use -w to ignore all whitespace, -b to ignore changes in the amount, and --ignore-blank-lines. For moved code, --color-moved colors blocks that moved and did not change, which makes large refactors readable. For prose and data files, --word-diff shows changes inside lines, which helps in documentation and in long single-line files.
Scripts should use --exit-code or --quiet, which makes diff exit with 1 when there are differences, so an if can test it. Add -z to separate names with NUL for safe parsing, since names can hold newlines and spaces, and use --no-ext-diff and --no-color so a configured external tool or color settings cannot change the output.
Choosing the right algorithm and reading hunks
Git can use several diff algorithms, and the choice changes how a change reads. The default is Myers, which is fast and sometimes produces a diff that splits a block awkwardly. --patience and --histogram choose better anchors in code that has repeated lines, such as closing braces, and give diffs that match how a person would describe the change. Set diff.algorithm to histogram if you read a lot of diffs, and the improvement is immediate in refactors. The hunk header carries the nearest function name after the line numbers, taken from a built-in pattern for the file type, and a .gitattributes entry with a diff driver improves it for languages the defaults miss.
Know when diff cannot help. Binary files show only that they differ, and a textconv filter configured for a file type can convert them to text for comparison, as is done for documents and images with metadata. Very large files make diff slow, and generated files bury the real change, so exclude them with a pathspec such as ':!*.lock' when reviewing.
See git-diff and the commit ranges section of Pro Git.


