Git, CI/CD & GitOpsVI · Index / Staging AreaIndex
The index explained — what the staging area is and why Git has one
What you'll learn
- Explain what the index is on disk and how it differs from the working tree and HEAD
- Articulate why Git has a separate staging area rather than committing directly from the working tree
- Read git status and map every line to a movement between the three trees
- Recognise the index as the unit of a commit and not the working tree
- Identify which commands read from or write to the index
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
The index is the most important Git concept that does not exist in other version-control systems. Subversion, Perforce, Mercurial’s older workflows, and most file-saver mental models treat a commit as “save what I have edited”. Git does not. Git treats a commit as “save what is currently in the index”. The working tree is where you edit; the index is what you are about to commit; the repository is what you have already committed. Get the index wrong and every commit is a surprise. Get it right and every commit is a deliberate, reviewable event.
What the index actually is
The index is a single binary file at .git/index. It is not a
directory, not a buffer in memory, not a copy of the files. It is
a serialised manifest: a flat list of every path the next commit
will contain, paired with the object ID of the blob that path
holds, plus the mode bits and a cache of stat() information Git
uses to decide whether the working tree file has been modified.
git ls-files --stage
# 100644 <blob-oid> 0 terraform/main.tf
# 100644 <blob-oid> 0 ansible/hosts.yml
# 100755 <blob-oid> 0 scripts/deploy.sh
The “0” column is the stage number. Stage 0 is the normal collapsed entry you see most of the time. Stages 1, 2, and 3 appear only during an unresolved merge and hold the merge base, ours, and theirs respectively (covered in lesson II-02).
The crucial property: the index is what gets committed, not the
working tree. When you run git commit, Git reads the paths and
blob IDs out of .git/index, constructs a tree object from them,
and writes a commit object pointing at that tree. The bytes on
disk in the working tree are not part of the commit unless they
have first been added to the index.
flowchart LR
WT["Working tree\n(filesystem edits)"] -- "git add" --> IDX["Index\n(.git/index manifest)"]
IDX -- "git commit" --> REPO["Repository\n(tree + commit object)"]
IDX -- "git diff --cached" --> HEAD["HEAD\n(last commit)"]
Why Git has a staging area
The two-area model — working tree and repository — is what Subversion and most other systems use. Git adds the index because a commit should be a deliberate event, not a side effect of saving a file. The index makes four things possible that a two-area model cannot:
- Partial commits. A file can have several logically independent changes inside it. The index lets you stage some changes and not others, so one commit can describe one logical change even when the working tree holds several.
- Review before commit. The contents of the next commit can
be inspected (
git diff --cached) before the commit happens. Pre-commit hooks, CI plan computation, and signed-off-by checks all read the index — they would have nothing to read in a two-area model. - Cross-file atomicity. A commit is the contents of the index at one moment. Multiple files staged together become one commit; partial stages across files let you describe one logical change that touches several files.
- External staging tools. Because the index is a file, tools
like
git add -p,git gui,git citool, and IDE front-ends can edit it without ever touching the working tree bytes.
Reading the three trees through git status
git status is the cheap way to see all three trees at once. It
compares the working tree to the index, and the index to HEAD,
and prints the differences as four named buckets:
git status
# On branch main
# Changes to be committed:
# modified: terraform/main.tf
# Changes not staged for commit:
# modified: ansible/hosts.yml
# Untracked files:
# playbooks/rotate-creds.yml
Each line names a state transition:
- Changes to be committed — the index differs from HEAD. The bytes are staged but not yet committed.
- Changes not staged for commit — the working tree differs
from the index. The file has been edited since the last
git add. - Untracked files — the working tree has a path that is not in the index at all.
- (Implicit) No changes — all three trees are aligned. The working tree matches the index and the index matches HEAD.
A common mistake is to read “Changes not staged for commit” as
“this file is not in the commit”. It is the opposite: the file is
in the index exactly as HEAD recorded it, but the working tree
has uncommitted edits that have not been added. Run git add and
the file moves into the “Changes to be committed” bucket.
The index is the unit of a commit
The single sentence to remember from this lesson: a commit is the contents of the index at one moment. Everything else — the working tree, the editor, the diff, the message — is preparation for that snapshot.
sequenceDiagram
participant Dev as Engineer
participant WT as Working tree
participant IDX as Index
participant REPO as Repository
Dev->>WT: edit terraform/main.tf
Dev->>IDX: git add terraform/main.tf
Note over IDX: index now holds the new blob
Dev->>IDX: git diff --cached (review)
Dev->>REPO: git commit
REPO->>REPO: write tree, write commit, advance ref
Note over WT,IDX: working tree and index are now identical to HEAD
If you take nothing else from this lesson, take this: when you
are about to commit, ask “what is in the index?” not “what have I
edited?”. The two are usually the same, but when they diverge —
because a previous git add left a stale change staged, because
an interactive staging tool was abandoned mid-edit, or because a
merge added an entry to the index — the commit will follow the
index, not the working tree.
Production discipline
- Read the index before you commit. A 30-second
git diff --cachedcatches the wrong file in the right commit. In an infrastructure repository the cost of a wrong commit is a wrong apply. - Trust
git status, not your editor. Your editor may show files as “saved” while the working tree still differs from the index.git statusis the only authoritative source of what the next commit will contain. - Treat the index as a checkpoint, not a buffer. The index
survives process exits, can be inspected, and can be replaced
with
GIT_INDEX_FILE. A pipeline that has staged a partial apply can be examined, paused, or reset by acting on the index directly.
Cross-course references
- Linux for Production Sysadmins - Part IX (Filesystem) covers the inode-level representation of a file; the index is the Git analogue of a directory entry, listing paths and the OIDs of their current blobs.
- Ansible for Production Sysadmins - Part XXXVII (RepoArch) treats the index as the boundary between untrusted working changes and the repository that the playbook will be run from.
- Terraform for Production Sysadmins - Part IX (State) covers the equivalent of the index for Terraform: the plan file, which is the candidate next state reviewed before apply.
Quiz
Knowledge check · 4 questions
Q1. An engineer has edited a Terraform file and run `git add terraform/main.tf`. They then run `git diff` and see nothing, but `git diff --cached` shows the full change. What does this tell them about the three areas?
Q2. The index is a transient buffer in memory that is discarded when the Git process exits.
Q3. Name the two storage locations a `git commit` reads from, and identify which one is the actual source of the commit.
Q4. Diagnose why a commit contains an unexpected change that the engineer insists they did not make in this session.
An engineer edits `terraform/main.tf`, runs `git add terraform/main.tf`, then `git commit -m 'fix bucket tags'`. The resulting commit also includes an unrelated change to `terraform/backend.tf` that the engineer did not touch. `git diff HEAD~1 -- terraform/backend.tf` shows the unexpected change in the commit. The engineer claims they only edited and staged `main.tf`.
Passing score: 75%. Answers are checked in this browser.