- Developers who already know basic commit and branch actions
- Readers who want to understand command boundaries and risk
Command Reference
git diff Tutorial
Explains how git diff compares working tree, index, and commit states, how to read the most useful forms, and how to avoid checking the wrong layer.
- A basic mental model of worktree, index, and commits
- Comfort reading `git status` and a small commit graph
- Using local cleanup commands on already shared history
- Continuing to rewrite before confirming a recovery path
The short version
git diff compares two states in Git, whether those states are the working tree, the index, or commits in history.
Most confusion around git diff comes from forgetting that the command is always relative. If the output looks surprising, the first thing to fix is usually the comparison target.
The three daily forms you should know first
git diff
git diff --staged
git diff HEAD
git diff: working tree versus indexgit diff --staged: index versusHEADgit diff HEAD: everything not yet represented inHEAD
If you only memorize one thing, memorize those three scopes.
Why this command matters
git diff is the command that keeps you honest before you:
- stage changes
- create a commit
- open a pull request
- rebase or reset
- explain what changed during an incident
In other words, diff is not only for review.
It is also for verification before costly actions.
Practical examples
Check what is still unstaged
git diff
Check what the next commit will contain
git diff --staged
Compare your feature branch to main
git diff main...feature/login
The triple-dot form is useful when you want the branch-specific change set, not every difference in absolute history position.
Inspect one file only
git diff -- src/search/doc-search.tsx
That keeps the output readable when the repository is noisy.
How to read “no output”
No diff output does not always mean “nothing changed.”
For example:
git diffmay be empty because the changes are already stagedgit diff --stagedmay be empty because nothing is staged yetgit diff HEADmay be large because it includes both staged and unstaged edits
That is why git status and git diff work best together.
Diagram view
A safer inspection routine
Before a commit or review, a reliable sequence is:
git statusgit diffgit diff --staged- only then commit, rebase, or open review
That routine catches a lot of avoidable mistakes:
- forgetting to stage a file
- staging too much
- reviewing the wrong branch comparison
- thinking a clean working tree means a clean staged snapshot
Many Git mistakes are not command mistakes. They are visibility mistakes. git diff is how you reduce them before they become history problems.
Boundaries and common mistakes
git diffdoes not mutate history, but it can still mislead you if you inspect the wrong comparison.- Long diff output does not automatically mean the change is too big; sometimes it means the scope is too broad.
- Review-oriented comparisons and staging-oriented comparisons are not the same question. Use the right form for the job.
If you compare the wrong refs, you can easily think a branch contains more or less than it really does. For pull request prep, verify whether you want two-dot or three-dot semantics before trusting the output.
Try it yourself
Advanced diff output formats
The default diff format is verbose and shows full context patches. Git offers several alternative output formats for different workflows.
--stat — summary of changes per file
Shows a compact per-file summary with insertions and deletions, plus an ASCII histogram:
git diff --stat
src/auth/login.ts | 15 +++++++--------
src/utils/helpers.ts | 3 +++
tests/login.test.ts | 42 ++++++++++++++++++++++++++++++++++++++++++
3 files changed, 54 insertions(+), 6 deletions(-)
Best for: quick code reviews and pull request summaries.
--numstat — machine-readable numbers
Outputs a tab-separated list with additions, deletions, and file path — easy to parse in scripts:
git diff --numstat
15 8 src/auth/login.ts
3 0 src/utils/helpers.ts
42 0 tests/login.test.ts
Best for: automated changelog generation and metrics collection.
--raw — low-level status codes
Shows the raw diff header with mode changes, SHA-1 hashes, and status letters (A/M/D/R/C):
git diff --raw
:100644 100644 ab12cd3 ef45gh6 M src/auth/login.ts
:000000 100644 0000000 7890abc A src/utils/helpers.ts
Best for: Git plumbing scripts and internal tooling.
| Flag | Human-readable | Script-friendly | Detail level |
|---|---|---|---|
| Default diff | Yes | No | Full patch |
--stat | Yes | Partial | Summary + histogram |
--numstat | Partial | Yes | Add/del counts |
--raw | No | Yes | SHA-1, mode, status |
Custom diff drivers via .gitattributes
Git lets you define custom diff behavior for specific file types through .gitattributes.
Define a custom diff driver
In .git/config:
[diff "word"]
command = git diff --word-diff
In .gitattributes:
*.md diff=word
*.txt diff=word
Binary file detection
Git automatically detects binary files and suppresses text diff output. You can override this:
*.png diff=exiftool
Then define an exiftool diff command to show image metadata differences instead.
Language-aware diffs
For minified or generated code, you can normalize content before diffing:
*.min.js diff=javascript
External diff tool integration
Replace Git's default unified diff with a richer visual tool by setting GIT_EXTERNAL_DIFF or configuring diff.external.
delta
delta adds syntax highlighting, side-by-side view, and line numbers:
git config --global core.pager delta
git config --global interactive.diffFilter "delta --color-only"
diff-so-fancy
diff-so-fancy highlights changed words within lines:
git config --global core.pager "diff-so-fancy | less --tabs=4 -RFX"
icdiff
icdiff provides side-by-side colored diff:
git config --global diff.external icdiff
Using difftool
Git also has a built-in difftool command that launches your configured GUI:
git config --global diff.tool vscode
git difftool HEAD~1 HEAD
Path-specific diff
You already saw git diff -- <path>, but there are more powerful path-scoping options:
Multiple paths
git diff -- src/ tests/
Glob patterns
git diff -- '*.test.ts'
Directory exclusion (no built-in flag, use combination)
git diff -- . ':!vendor/'
Diff from a subdirectory
When you run git diff inside a subdirectory, paths are relative to that directory. Use --no-prefix to see full paths:
git diff --no-prefix -- src/
Historical range diff
Compare a file across arbitrary commits
git diff v1.0..v2.0 -- README.md
Three-dot range for merge-base comparison
git diff main...feature -- src/
This shows changes from the merge base of main and feature to the tip of feature — exactly what a pull request would introduce.
Diff between two commits (ignoring working tree)
git diff abc123 def456
Diff a commit against its parent
git diff HEAD~1 HEAD
# or simply:
git show HEAD
Word-level diff
When line-level diff is too coarse, --word-diff highlights changes at the word level:
git diff --word-diff
Output uses color and markers: [-removed-] and {+added+}.
Word-diff variants
# Color-only highlighting (best for terminals)
git diff --word-diff=color
# Porcelain format (for scripts)
git diff --word-diff=porcelain
# Plain markers
git diff --word-diff=plain
Regex-based word boundaries
Customize what counts as a "word":
git diff --word-diff-regex="[^[:space:]]+"
This treats every non-whitespace character as its own word boundary, giving character-level diff granularity.
The goal is to build reflexes for which diff form answers which question.
git switch -c lab/diff-demo # edit one file and stage only part of the changeTry it
- Run `git diff` and note what is still unstaged.
- Run `git diff --staged` and see what the next commit contains.
- Run `git diff HEAD` and compare how it combines both views.
- If you have a main branch baseline, try `git diff main...HEAD`.
- You will see that each diff form answers a different question.
- The same file can appear differently depending on whether you compare the working tree, the index, or branch history.
- You become less likely to commit or review the wrong content.
- If `git diff` is empty, check whether the changes are already staged.
- If a branch diff feels too large, confirm you used the intended ref range.
- If the patch is unreadable, narrow it to a path or smaller scope before making decisions.