Git, CI/CD & GitOpsVII · Repository InspectionInspection
git diff three ways — working tree, index, and HEAD
What you'll learn
- Distinguish the three default diffs: working tree vs index, index vs HEAD, and working tree vs HEAD
- Use --cached to compute the diff between the index and HEAD
- Use --stat to summarise a diff and --word-diff for inline highlights
- Use the <a>..<b> syntax to compute the diff between any two commits
- Recognise the right diff for each production case: pre-commit, staged preview, deployed state
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
git status answers the question “are there changes?”. git log
answers the question “what was committed?”. git diff answers the
question “what changed, byte by byte?” — but the answer depends on
which two trees are being compared. The default git diff compares
the working tree to the index, which is rarely the question an
infrastructure engineer is asking. The three possible diffs in a
Git repository are:
- Working tree vs index — the default
git diff. The diff of unstaged changes. The bytes that will be discarded bygit restore <path>. - Index vs HEAD —
git diff --cached(or--staged). The diff of staged changes. The bytes that will be recorded in the next commit. - Working tree vs HEAD —
git diff HEAD. The full diff of every change on top of the last commit, staged or not. The bytes that the next commit will not record unless the unstaged changes are first staged.
The three diffs answer three different production questions. The wrong choice is the difference between “I see what I am about to commit” and “I see what I would have committed if I had remembered to stage everything”.
The three diffs in detail
The default git diff (no arguments) compares the working tree to
the index. Every line that differs between the two is printed. The
diff is the unstaged change: the bytes that have been edited in
the working tree but not yet promoted to the index.
git diff
# diff --git a/terraform/main.tf b/terraform/main.tf
# index 1b2c3d4..5e6f7a8 100644
# --- a/terraform/main.tf
# +++ b/terraform/main.tf
# @@ -12,7 +12,7 @@ module "router" {
# source = "git::ssh://git@github.com/example/router.git"
# - version = "3.2.6"
# + version = "3.2.7"
# }
git diff --cached (synonym: --staged) compares the index to
HEAD. The diff is the staged change: the bytes that will be
recorded in the next commit. This is the diff a CI pipeline
should gate on, because it is the diff that the commit will
produce.
git diff --cached
# (same output as the previous example, because the change was
# already staged)
git diff HEAD compares the working tree to HEAD. The diff is the
combined change: every byte that differs between HEAD and the
working tree, regardless of whether the bytes are staged. This is
the diff a code-review tool typically shows, because it shows
everything the next commit could produce, including unstaged
edits.
flowchart LR
WT["Working tree"] -->|"git diff (default)"| IDX["Index"]
IDX -->|"git diff --cached"| HD["HEAD"]
WT -->|"git diff HEAD"| HD
HD -->|"git diff <a>..<b>"| ANY["Any commit"]
WT -->|"git diff <a>..<b>"| ANY
The three diffs are the three views of “what changed?”. The correct view depends on the question:
- Pre-commit, no staging yet:
git diffshows the unstaged edits. The right command for “did my editor save the file correctly?”. - Post-staging, pre-commit:
git diff --cachedshows the candidate commit. The right command for “what will the next commit record?”. - Pre-commit, mixed staging:
git diff HEADshows everything on top of HEAD, regardless of staging. The right command for “what is the complete set of changes I have made?”.
Diffing any two commits
The <a>..<b> syntax compares two arbitrary revisions. The
diff is the change in the tree at <b> relative to the tree at
<a>. The order matters: <a>..<b> is the diff from <a>
to <b>, which is the changes that were applied by commits
between <a> and <b>.
# Diff between two tags
git diff v3.2.6..v3.2.7 -- terraform/
# diff --git a/terraform/main.tf b/terraform/main.tf
# ...
# Diff between main and a feature branch
git diff main..feature/edge-mtu -- terraform/
# Diff between a commit and its parent (same as git show <rev>)
git diff 8a3f9d2~1..8a3f9d2 -- terraform/main.tf
The <a>..<b> syntax is the right primitive for “what changed
between two releases?” or “what is on this branch that is not on
main?”. The <a>...<b> (three-dot) syntax is different: it shows
the diff between the merge base of <a> and <b> and <b>,
which is the diff of the changes made on the branch since it
diverged from <a>.
# Three-dot: what did the feature branch change since diverging from main?
git diff main...feature/edge-mtu -- terraform/
The two-dot and three-dot forms are the same when the second argument is a direct ancestor of the first. They differ when the two arguments have diverged: the two-dot shows the diff between the two tips, the three-dot shows the diff of the branch’s contribution since the merge base.
Summary and word-level diffs
Two flags change the rendering of a diff:
--stat— append a summary table to the diff. Each changed file is listed with the number of additions and deletions, and a histogram bar. The full diff is still printed; the--statis an additional section at the end.git diff --stat HEAD~3..HEAD # terraform/main.tf | 2 +- # modules/router/versions.tf | 2 +- # deploy/prod/main.tf | 12 ++++++------ # 3 files changed, 8 insertions(+), 8 deletions(-)--word-diff— change the inline renderer from line-oriented (-and+lines) to word-oriented ([-old-]{+new+}inline markers). The right form for prose files (Markdown, YAML, documentation) where a single line is not a unit of change. Add--word-diff=colorfor terminal-friendly highlighting.git diff --word-diff HEAD~3..HEAD -- README.md
The --stat flag is the right summary for a CI log and a code
review. The --word-diff flag is the right form for inspecting
documentation and configuration changes where one line is not
meaningful.
flowchart LR
D["git diff"] --> S["--stat"]
D --> W["--word-diff"]
D --> N["--name-only"]
D --> C["--cached"]
D --> H["-- HEAD"]
S --> Use1["CI summary, PR review"]
W --> Use2["docs, YAML, prose"]
N --> Use3["scripts that need file list"]
C --> Use4["candidate commit"]
H --> Use5["combined view"]
The production diff decision tree
For every “what changed?” question, the answer is one of:
| Question | Command |
|---|---|
| What did I just edit? | git diff |
| What will the next commit record? | git diff --cached |
| What is the full set of my pending changes? | git diff HEAD |
| What changed between two releases? | git diff <tag1>..<tag2> |
| What did this branch change since diverging? | git diff <base>...<branch> |
| What changed in this one commit? | git show <rev> |
| What is the diff of one path at one commit? | git show <rev> -- <path> |
The first three are the everyday forms. The fourth and fifth are the release and branch forms. The last two are the single-commit forms covered in the previous lesson.
Production discipline
- Decide which diff you want before the command. “What
changed?” is ambiguous; the three diffs answer three different
questions. The production rule is to name the question out
loud: “what will the commit record?” →
--cached. - Gate CI on
--cached, not on the working tree. The CI pipeline lints the bytes that will be recorded, not the bytes that the engineer happens to have on disk. - Use two-dot for release notes, three-dot for branch diffs. The two-dot answers “what changed between X and Y?”; the three-dot answers “what did this branch contribute?”.
- Use
--statfor CI logs and summaries. The histogram and the file list are the right shape for a CI summary; the full diff is for the inspector. - Use
--word-difffor prose and YAML. Line-oriented diffs are not meaningful for documentation and configuration files where a single line is not a unit of change.
Cross-course references
- Linux for Production Sysadmins - Part IX (Filesystem) distinguishes the same three views of “what is on disk?”: working tree, index, and committed state.
- Ansible for Production Sysadmins - Part XXXVII (RepoArch)
uses
git diff --cachedas the canonical pre-commit lint target for Ansible playbooks. - Terraform for Production Sysadmins - Part IX (State) uses
git diff main..HEAD -- terraform/as the canonical “what is in this plan?” query beforeterraform apply.
Quiz
Knowledge check · 4 questions
Q1. A CI pipeline lints the bytes that will be recorded in the next commit. Which diff command is the correct lint target?
Q2. The two-dot syntax `git diff <a>..<b>` and the three-dot syntax `git diff <a>...<b>` produce the same output whenever the two revisions have diverged (neither is an ancestor of the other).
Q3. Name the three trees Git uses to compute a diff, and give the command for each of the three diffs.
Q4. Diagnose why a CI pipeline silently approved a Terraform change that the engineer did not see, and recommend the right diff command for the pipeline.
An engineer modifies `terraform/main.tf` to bump a module version. The edit is visible in their editor but not staged. The CI pipeline runs `git diff` (the default, working tree vs index) and sees the change. The pipeline approves the change. The engineer then runs `git commit` without `--all`, staging only some files. The committed change does not include the module version bump — only the unstaged view saw it. The CI pipeline's approval was actually for the unstaged bytes, which were discarded by the commit.
Passing score: 75%. Answers are checked in this browser.