Skip to main content
RunBook Academy

Git, CI/CD & GitOpsVI · Index / Staging AreaIndex

The index as a commit preview — git diff --cached and the review before commit

Intermediate⏱ ~17 mingit

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

Not yet marked complete on this device.

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:

  1. A file removed with rm but 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 commit will record the deletion and the blob will become unreachable.
  2. 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 commit will 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 commit completes. The hook reads the index (via git diff --cached) and can refuse the commit if a check fails: signed-off-by missing, debug print() 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

  1. Make git diff --cached the last command before git commit. The 30 seconds it takes is the cheapest audit tool in the entire workflow.
  2. 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.
  3. 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 --cached produces; the same format is what patch -p1 --dry-run consumes 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 --cached shows, 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

  1. 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?

  2. Q2. `git diff --cached` and `git diff --staged` are different commands that compare different pairs of trees.

  3. Q3. Name the diff form that pre-commit hooks and CI pipelines should read to determine what will be committed, and explain why.

  4. 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.