Git, CI/CD & GitOpsVI · Index / Staging AreaIndex
The index as a commit preview — git diff --cached and the review before commit
What you'll learn
- Read git diff --cached as a preview of the next commit
- Explain why the commit follows the index, not the working tree
- Use git diff --cached in CI to gate a push or a merge
- Recognise why pre-commit hooks and lint-staged operate on the index
- Build a three-step preview discipline: status, --cached, commit
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
Every other version-control system commits the working tree.
Git does not. Git commits the index. This is the design choice
that makes git diff --cached possible — and git diff --cached is the single most useful command in a production
Git workflow. It is the difference between a commit that
describes one logical change and a commit that describes
whatever happened to be on disk at the moment the engineer
typed git commit. This lesson turns the index from a
staging buffer into a review surface.
What git diff —cached shows
git diff --cached (equivalent to git diff --staged) compares
the index to HEAD. The output is exactly the diff that the next
commit will introduce: every added line, every removed line,
every renamed file, every mode change. If the next commit is
the right one, git diff --cached is the right diff. If the
next commit is the wrong one, git diff --cached shows exactly
what is wrong.
git add terraform/main.tf
git diff --cached
# diff --git a/terraform/main.tf b/terraform/main.tf
# @@ -12,7 +12,7 @@ resource "aws_s3_bucket" "logs" {
# bucket = "prod-logs"
# - acl = "private"
# + acl = "log-delivery"
# tags = {
# Owner = "platform"
The same command without --cached would compare the working
tree to the index. After git add, the working tree and the
index are identical, so git diff (no args) shows nothing. The
staged change is hidden from the default diff and visible only
through --cached. This is the most common confusion in the
three-trees model: “I see nothing in git diff after staging,
so the change is gone” — no, the change is in --cached.
flowchart LR
A["git diff"] --> B["working tree vs index\n(uncommitted edits not yet staged)"]
C["git diff --cached"] --> D["index vs HEAD\n(the next commit, exactly)"]
E["git diff HEAD"] --> F["working tree vs HEAD\n(everything since the last commit)"]
Why the commit follows the index, not the working tree
git commit reads the index, constructs a tree object from the
index entries, and writes a commit object pointing at that
tree. The working tree bytes are not read. A file that is in
the working tree but not in the index is not in the next
commit. A file that is in the index but not in the working tree
is in the next commit (the commit will produce a blob whose
content matches the index entry, not the working tree bytes).
The asymmetry has two practical consequences:
- A file removed with
rmbut still in the index will commit as a deletion. The index entry still names the blob; the working tree no longer has the path.git commitwill record the deletion and the blob will become unreachable. - A file written to the working tree but never
git added will not commit. The bytes are on disk; the index does not know about them.git commitwill not see them.
Both consequences are the reason git diff --cached is the
review surface. The engineer runs it, sees what will be
committed, and catches both kinds of surprise before the
commit happens.
The three-step preview discipline
A 30-second habit that catches every wrong commit:
git status # what is staged, what is not, what is untracked
git diff --cached # what will be committed, exactly
git commit -m '...' # only after the first two confirm the intent
The first command reads the three-trees state at a glance: the “Changes to be committed” bucket names the staged files; the “Changes not staged” bucket names the working-tree drift; the “Untracked files” bucket names the surprises. If the first bucket contains anything the engineer did not stage, the three-step halts here.
The second command reads the actual diff. This is where a
debug artifact, a stale change from a previous session, or a
local environment override is most likely to be visible. A
reviewer reading the commit message alone cannot catch what
the engineer missed; the engineer reading git diff --cached
can.
The third command is the commit itself. By the time it runs, the engineer has confirmed that the index holds exactly the changes they intended, and the audit trail is correct.
sequenceDiagram
participant Eng as Engineer
participant WT as Working tree
participant IDX as Index
participant REPO as Repository
Eng->>WT: edit files
Eng->>IDX: git add PATHS
Eng->>Eng: git status (review)
Eng->>Eng: git diff --cached (preview)
Eng->>REPO: git commit
Note over REPO: commit matches the preview
Why every guard rail operates on the index
The Git ecosystem’s guard-rail tools all operate on the index because the index is what gets committed. A few examples:
- Pre-commit hooks. A hook is a script that runs before
git commitcompletes. The hook reads the index (viagit diff --cached) and can refuse the commit if a check fails: signed-off-by missing, debugprint()present, a path matching a secret pattern, a Terraform plan that would destroy a resource. - lint-staged. A Node.js tool that runs linters against staged files only. It reads the index, runs the linter, and re-stages any auto-fixed output. Working-tree files that are not staged are not linted, because they will not be committed.
- CI plan attachments. A pipeline that attaches a Terraform plan to the commit description must derive the plan from the index, not from the working tree, because the plan must describe exactly what the commit will apply.
- Signed-off-by and DCO. The Developer Certificate of
Origin workflow checks that the commit message carries a
Signed-off-by:trailer. The check reads the message that the index-derived commit will produce.
Each tool depends on the same property: the index is the unit of the commit. If the commit read from the working tree, none of these tools would have a stable surface to read from.
Production discipline
- Make
git diff --cachedthe last command beforegit commit. The 30 seconds it takes is the cheapest audit tool in the entire workflow. - Make CI gate on
--cached, not on the working tree. A pipeline that checks the working tree is checking a different commit than the one that will land. - Make pre-commit hooks read from the index. The hook’s contract is “this commit will be made if I exit zero”. The only way to honour that contract is to read the index, not the working tree.
Cross-course references
- Linux for Production Sysadmins - Part XV (DiffAndPatch)
covers the unified-diff format that
git diff --cachedproduces; the same format is whatpatch -p1 --dry-runconsumes and what a code-review tool renders. - Ansible for Production Sysadmins - Part XXXVII (RepoArch)
treats the staged diff as the contract between the playbook
author and the reviewer: the reviewer sees exactly what
--cachedshows, no more, no less. - Terraform for Production Sysadmins - Part X (Plan) is
the most explicit application of this principle: the plan
file is the index-equivalent for Terraform, and it is what
is reviewed before
terraform apply.
Quiz
Knowledge check · 4 questions
Q1. An engineer has staged a change with `git add terraform/main.tf`, runs `git diff` and sees nothing, then commits. The commit includes a debug `print()` line that the engineer did not intend to ship. What command would have caught the debug line before the commit?
Q2. `git diff --cached` and `git diff --staged` are different commands that compare different pairs of trees.
Q3. Name the diff form that pre-commit hooks and CI pipelines should read to determine what will be committed, and explain why.
Q4. Identify why a CI plan attached to a Terraform commit does not match the change that landed in production, and choose the right fix.
A team's CI pipeline runs `terraform plan` against the working tree, attaches the plan to the commit description, and merges the pull request. Six months later, an incident reveals that the deployed Terraform state does not match the plan attached to the commit. The CI logs show the plan was computed against the working tree at push time, not against the index.
Passing score: 75%. Answers are checked in this browser.