Make git diff do the heavy lifting so you don’t have to.
TL;DR
- Use the right point of comparison:
git diff,git diff --staged, andgit diff HEADanswer different questions. - Make diffs more readable with word‑level views:
--word-diffand--word-diff-regex. - Hunt changes by text or regex:
-S(string) and-G(regex) narrow the diff to relevant hunks. - Compare branches the right way:
A..BvsA...Bhave different meanings. - Ignore noise: whitespace flags and
.gitattributeskeep diffs focused on what matters.
1) Pick the right comparison: working tree vs index vs HEAD
Why it matters: git diff can compare three different things: your working files, the staging area (index), and the last commit (HEAD). Knowing which one you’re looking at prevents “Where did my change go?” moments.
How to use it
|
1 2 3 4 5 6 7 8 9 |
# What changed in your working tree that is NOT staged yet git diff # What you have staged (index) compared to HEAD (what will be committed) git diff --staged # or: git diff --cached # Everything since HEAD, staged and unstaged (useful sanity check) git diff HEAD |
Example
Suppose you edited app.py twice and only staged one hunk:
|
1 2 3 |
$ git diff -- app.py # shows the unstaged hunk(s) $ git diff --staged app.py # shows only what is staged and will commit |
Sample output (trimmed):
|
1 2 3 4 |
@@ -17,7 +17,7 @@ def handler(): - result = do_thing(verbose=False) + result = do_thing(verbose=True) |
That hunk appears in --staged if you added it; if not, you’ll only see it in plain git diff.
When to reach for it: Before committing, run git diff --staged to verify exactly what’s about to land. If something’s missing, you forgot to git add it.
Pro tip: Add a short alias so you can’t forget:
|
1 2 |
git config --global alias.dh 'diff HEAD' |
2) Make diffs readable with word‑level views
Why it matters: Line diffs are fine for code, but for prose, JSON, or long lines, word‑level diffs show precisely what changed.
How to use it
|
1 2 3 4 5 6 7 8 9 |
# Inline word markers (default style) git diff --word-diff # Cleaner side markers better for copy/paste git diff --word-diff=porcelain # Tune what a "word" is (e.g., treat snake_case parts as words) git diff --word-diff-regex='[^_\W]+' |
Example
You changed a long sentence in README.md from:
Run the server with
--port 8080and--debugto see verbose logs.
…to:
Run the server with
--port 8080and--verboseto see detailed logs.
Word‑diff makes the change obvious:
|
1 2 |
git diff --word-diff README.md |
Sample output (inline markers show deletions/insertions):
|
1 2 |
Run the server with `--port 8080` and `--[-debug-]{+verbose+}` to see {+detailed+} logs. |
Pro tip: Pair with --color-moved to highlight code that was only relocated:
|
1 2 |
git diff --color-moved |
3) Hunt for specific changes with -S and -G
Why it matters: In large diffs, you often care about where a symbol or pattern changed. -S and -G narrow the diff to only those hunks.
How to use it
|
1 2 3 4 5 6 7 8 9 |
# Show only hunks that add/remove the exact string "doThing" git diff -S doThing # Show hunks where a regex matches (e.g., any function definition of do_*) git diff -G 'def do_\w+' # Combine with pathspecs to focus on directories git diff -G 'TODO' -- src/ docs/ |
Example
Find where an API route changed from v1 to v2:
|
1 2 |
git diff -G '"/api/v[12]/users"' -- server/routes.py |
Or spotlight altered function definitions in Python:
|
1 2 |
git diff -G "def do_" |
When to reach for it: Audits (e.g., “Where did we touch auth?”) or PR reviews (e.g., “Show me only the places we changed this function’s signature”).
Pro tip: Use -G to detect structural changes like opening hours changing in a YAML file:
|
1 2 |
git diff -G '^\s*opening_hours:' config/app.yml |
4) Compare branches correctly: .. versus ...
Why it matters: git diff main..feature and git diff main...feature are not the same. One compares tips; the other compares from the merge base.
How to use it
|
1 2 3 4 5 6 |
# Changes reachable from feature that aren't in main (tip-to-tip) git diff main..feature # Changes in feature since it diverged from main (from merge-base) git diff main...feature |
Example
You branched feature/login from main a week ago. Since then, main picked up unrelated commits (docs, CI tweaks). To preview what your PR really introduces, use the merge‑base comparison:
|
1 2 |
git diff main...feature/login |
If you instead run:
|
1 2 |
git diff main..feature/login |
…you’ll also see changes that happened on main after the branch, which is often noisy for review.
When to reach for it:
- Use
main...featureto review what the branch truly introduces beyond where it forked—perfect for PR prep. - Use
main..featureto see net differences between the tips (includes changes onmaintoo).
Pro tip: You can diff commits by hashes, tags, or upstream tracking branches the same way:
|
1 2 |
git diff v2.1.0...v2.2.0 |
5) Ignore noise so real changes stand out
Why it matters: Whitespace normalization, formatter reflows, or generated files can bury meaningful edits.
How to use it
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# Ignore whitespace changes in lines that otherwise match git diff -w # Ignore changes in the amount of whitespace at EOL git diff --ignore-space-at-eol # Ignore all whitespace (rare; can hide real changes) git diff -b # Show only names or stats if you just need a high-level view git diff --name-only git diff --stat |
Example
You upgraded a formatter that rewrapped many lines without meaningfully changing code. Compare logical changes while downplaying whitespace churn:
|
1 2 |
git diff --word-diff --ignore-space-change -- src/ |
Or keep lockfiles out entirely:
|
1 2 |
git diff -- ':!**/package-lock.json' ':!**/poetry.lock' |
Tame noisy files with .gitattributes: Mark generated assets so they don’t flood reviews.
|
1 2 3 4 5 |
# .gitattributes *.min.js -diff *.lock -diff *.svg text diff=svg |
Then define a custom driver in your Git config if needed:
|
1 2 3 4 |
# ~/.gitconfig [diff "svg"] textconv = rsvg-convert --keep-aspect-ratio --format=txt |
Pro tip: If your formatter rewrites entire files (e.g., Prettier), compare logical changes with word‑diff:
|
1 2 |
git diff --word-diff --ignore-space-change |
Bonus quick hits
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# Only show function names that changed (C-like languages) git diff --function-context # or: -W to show the whole function # Limit by file type (pathspec magic) git diff -- '*.py' ':!tests/**' # Review your last commit vs HEAD^ (what you just did) git diff HEAD^! # Summarize renames/mode changes without content git diff --summary |
Putting it together: a practical review flow
- Stage intentionally:
git add -pto pick hunks. - Verify exactly what will commit:
git diff --staged. - Tighten focus: add
-S symbolor-G patternas needed. - Reduce noise: tack on
-wor leverage.gitattributes. - Branch review: use
main...featurebefore opening a PR.
With these five tricks, git diff transforms from a wall of red/green into a targeted lens on what actually changed.




