Git Diff-Tree Guide | Generated by AI
Here’s a practical, end-to-end guide to git diff-tree—what it is, when to use it, and the options you’ll actually reach for in real work. I’ll avoid big tables and stick to focused explanations with copy-pasteable examples.
What git diff-tree actually does
- It compares tree objects. In everyday use, that usually means: “show me what changed in a commit compared to its parent(s).”
- It’s plumbing (script-friendly), unlike
git show/git logwhich are porcelain (human-oriented). - It never looks at your working directory; it only compares trees stored in the repo (commits, tags pointing to commits, or raw tree IDs).
Basic forms you’ll use
- Diff a commit against its parent
git diff-tree -p <commit>
If <commit> has one parent, you see a normal patch. If it’s a merge commit, you’ll see nothing unless you ask for merges (see below).
- Diff two trees/commits explicitly
git diff-tree -p <old-tree-or-commit> <new-tree-or-commit>
Great when you want to compare any two points, not just “commit vs parent”.
- Show only file names (no patch)
git diff-tree --name-only -r <commit>
Add -r to recurse into subdirectories so you get a flat list.
- Show names with change type
git diff-tree --name-status -r <commit>
# Outputs lines like:
# A path/to/newfile
# M path/to/modified
# D path/to/deleted
- Show a patch (full diff)
git diff-tree -p <commit> # unified diff like `git show`
git diff-tree -U1 -p <commit> # less context (1 line)
Must-know options (with why/when)
-r— Recurse into subtrees so you see all nested paths. Without it, a directory that changed might show as a single line.--no-commit-id— Suppress the “commit” header when you’re scripting per-commit output. --root— When a commit has no parent (initial commit), still show its changes vs the empty tree.-m— For merge commits, show diffs against each parent (produces multiple diffs).-c/--cc— Combined merge diff.--ccis a refined view (whatgit showuses for merges).--name-only/--name-status/--stat/--numstat— Different summary styles.--numstatis script-friendly (added/removed line counts).--diff-filter=<set>— Filter by change types, e.g.--diff-filter=AM(only Added or Modified); common letters:Add,Modified,Deleted,Renamed,Copied,Type changed.-M/-C— Detect renames and copies. Add an optional similarity threshold, e.g.-M90%.--relative[=<path>]— Restrict output to a subdirectory; without an argument, it uses the current working dir.-z— NUL-terminate paths for unambiguous machine parsing (handles newlines or tabs in filenames).--stdin— Read a list of commits (or pairs) from standard input. This is the secret sauce for fast batch operations.
Canonical scripting patterns
1) List changed files for a single commit
git diff-tree --no-commit-id --name-status -r <commit>
2) Batch over many commits (fast!)
git rev-list main --since="2025-08-01" |
git diff-tree --stdin -r --no-commit-id --name-status
--stdin avoids spawning git per commit and is much faster for large ranges.
3) Only additions and modifications in a directory
git diff-tree -r --no-commit-id --name-status \
--diff-filter=AM <commit> -- src/backend/
4) Count lines added/removed per file (script-friendly)
git diff-tree -r --no-commit-id --numstat <commit>
# Outputs: "<added>\t<deleted>\t<path>"
5) Detect and show renames in a commit
git diff-tree -r --no-commit-id -M --name-status <commit>
# Lines like: "R100 old/name.txt\tnew/name.txt"
6) Patch for a merge commit
git diff-tree -m -p <merge-commit> # per-parent patches
git diff-tree --cc <merge-commit> # combined view (single patch)
7) Initial commit (no parent)
git diff-tree --root -p <initial-commit>
Understanding the raw record format (if you parse by hand)
Use --raw (implicitly used by some modes) to get minimal, stable records:
:100644 100644 <oldsha> <newsha> M<TAB>path
- Numbers are file modes:
100644regular file,100755executable,120000symlink,160000gitlink (submodule). - Status is a single letter (
A,M,D, etc.), possibly with a score (e.g.,R100). - For renames/copies you’ll see two paths. With
-z, fields are NUL-separated; without-z, they’re tab-separated.
Tip: If you are building reliable tooling, always pass -z and split on NUL. Filenames with newlines exist.
Comparing git diff-tree to related commands (so you pick the right one)
git diff: compares index/working tree vs HEAD or any two commits/trees; interactive development.git show <commit>: a pretty wrapper for “diff vs parent + metadata.” Great for humans.git log -p: history plus patches. For ranges, it’s often more convenient than manually loopingdiff-tree.git diff-tree: plumbing for precise, scriptable per-commit diffs, batchable with--stdin.
Real-world examples
“What changed in this PR merge commit?”
git diff-tree --cc <merge-commit> | less
If you need parent-wise detail:
git diff-tree -m -p <merge-commit> | less
“Feed a CI step a clean list of files modified by the latest commit”
git diff-tree --no-commit-id --name-only -r HEAD > changed.txt
“Only Java files added or modified in the last 20 commits”
git rev-list -n 20 HEAD |
git diff-tree --stdin -r --no-commit-id --name-only --diff-filter=AM |
grep -E '\.java$'
“Summarize churn (added/removed lines) for a release tag”
git diff-tree -r --no-commit-id --numstat v1.2.0..v1.3.0
“Handle weird filenames safely”
git diff-tree -z -r --no-commit-id --name-status <commit> |
awk -v RS='\0' 'NR%2{status=$0; next}{printf "%s %s\n", status, $0}'
Performance notes
- Prefer
--stdinwithgit rev-listfor big ranges; it avoids process churn. - Skip patches (
--name-only/--name-status) when you don’t need line changes. - Avoid rename detection (
-M,-C) if you care about speed over accuracy.
Gotchas & edge cases
- No output for merges by default. You must pass
-m(per-parent) or--cc/-c(combined). - Initial commit is “empty tree vs tree”. Use
--rootto force a diff. - Submodules show mode
160000and a SHA (gitlink). You won’t see inner diffs unless you diff inside the submodule repo. - Pathspecs are after
--. If you filter by paths, put them after--to avoid ambiguity. - Filenames with whitespace/newlines. Always add
-zif you’re parsing.
Quick “cheat snippets” you’ll actually reuse
# Files changed (flat list)
git diff-tree --no-commit-id --name-only -r <commit>