Understanding the Git Index (the Staging Area)
The index is a single binary file at .git/index that holds a complete description of your next
commit. Not a list of filenames you have flagged — a full content listing, with one entry per tracked
path recording that path’s mode, its metadata, and the object ID of its exact content.
Most introductions call it “the staging area” and stop there. That name describes what you use it for, not what it is, and the gap between the two is where the confusion lives.
Three names, one thing
Section titled “Three names, one thing”You will see all three in documentation and error messages. They are the same object:
- The index — the implementation name, used in Git’s source,
.git/index, and plumbing commands. - The staging area — the conceptual name, describing its role in the workflow.
- The cache — a historical name that survives in flags like
git diff --cachedandgit rm --cached.
git diff --cached and git diff --staged are literally synonyms.
What the index actually contains
Section titled “What the index actually contains”Here is the index of a small repository, printed with a plumbing command:
git ls-files -sWhat it doesLists the files in the index along with each entry's file mode, blob object ID and merge stage number.
Why we run itIt shows the index as data rather than as workflow. This is the clearest way to see that the index stores content references, not filenames.
Expected resultOne line per tracked path: mode, 40-character object ID, stage number, then the path.
100644 0c2aa38e0600e0d2df09c2f84664d8a14f899879 0 app.txt100644 f44d1e3cfdd89d6741e19a2566840a4fba2eb54a 0 lib/util.txtReading one line:
| Field | Value | Meaning |
|---|---|---|
| Mode | 100644 | A regular, non-executable file. 100755 is executable; 120000 is a symbolic link |
| Object ID | 0c2aa38… | The blob holding this file’s exact content |
| Stage | 0 | Normal, unconflicted entry. Stages 1–3 appear only during a merge conflict |
| Path | app.txt | The path, relative to the repository root |
Two things stand out.
The index stores an object ID, not the file. The content itself already lives in the object
database — git add put it there. The index just points at it.
The paths are flat. There is no nesting: lib/util.txt is one entry with a slash in its name, not a
directory entry containing a file entry. The hierarchy is constructed at commit time, when Git builds
tree objects from these flat paths.
The index also stores filesystem metadata for each entry — size, modification time, inode — which is
how git status avoids re-reading every file on every invocation. If a file’s size and timestamp are
unchanged, Git trusts that its content is unchanged:
git ls-files --debugapp.txt ctime: 1787403563:343431639 mtime: 1787403563:343431639 dev: 64769 ino: 786547 uid: 1000 gid: 1000 size: 74 flags: 0What the index represents
Section titled “What the index represents”The index is a proposed snapshot. It answers one question: if I committed right now, what exactly would be recorded?
That framing explains its position between the other two areas:
Three columns. On the left, the working tree, holding the file app.txt with content that includes a new debug line. In the middle, the index, holding app.txt without the debug line — the version that was staged. On the right, HEAD, holding the original app.txt. Two labelled comparisons connect them: git diff compares the working tree to the index, and git diff --staged compares the index to HEAD.
Right after a commit, all three agree. As you work, the working tree drifts. As you stage, the index catches up with the working tree and pulls away from HEAD. Committing collapses all three back together.
What git add does to the index
Section titled “What git add does to the index”Three operations, in order:
-
Read the file’s current bytes from the working tree.
-
Write a blob into the object database, named by the hash of those bytes. If an identical blob already exists, nothing new is written.
-
Update that path’s index entry to point at the blob, and refresh the cached filesystem metadata.
The critical detail is step 1: git add reads the file at that moment. The index then holds a
reference to that specific content, frozen. Later edits to the file do not follow it.
You can watch this directly:
git ls-files -s app.txt # index versiongit ls-tree HEAD app.txt # committed version100644 734e3f3decb2995c4d8674c27b99740d516edce5 0 app.txt100644 blob 0c2aa38e0600e0d2df09c2f84664d8a14f899879 app.txtTwo different object IDs for the same path: the staged content and the committed content. That
difference is what git diff --staged renders as a diff.
Why Git has an index at all
Section titled “Why Git has an index at all”Mercurial and several other version control systems commit the working tree directly. Git’s extra step is a deliberate design choice, and it buys three things.
1. Commits can be smaller than your changes
Section titled “1. Commits can be smaller than your changes”Real work is messy. In one session you might fix a bug, correct a typo in a comment, and start an unrelated refactor. Committing the working tree would force all three into one commit, or force you to undo two of them temporarily.
The index lets you commit the bug fix alone, then the typo, then the refactor — three readable commits from one messy working tree. History that reads as a sequence of intentional changes is enormously more useful than history that records when you happened to save.
2. Conflict resolution has somewhere to live
Section titled “2. Conflict resolution has somewhere to live”During a merge conflict, the index holds up to three versions of each conflicted path, distinguished by their stage number:
100644 0c2aa38e0600e0d2df09c2f84664d8a14f899879 1 app.txt100644 9f4e345b1f1ccf77e903287c99e5d231cbaf1ad2 2 app.txt100644 734e3f3decb2995c4d8674c27b99740d516edce5 3 app.txt| Stage | Version |
|---|---|
| 1 | The common ancestor — the last commit both branches shared |
| 2 | “Ours” — the version on the branch you are merging into |
| 3 | “Theirs” — the version on the branch you are merging in |
This is why git status can describe a conflict precisely, and why git checkout --ours and
--theirs can pull out a specific side. Staging your resolution with git add collapses the three
entries back to a single stage-0 entry — which is exactly what “resolving” means mechanically, and why
git add is how you mark a conflict resolved.
3. Committing is fast
Section titled “3. Committing is fast”Because the index already holds the object ID for every path, git commit does not need to read or
hash your files. It builds tree objects from IDs the index already knows. In a repository with a
hundred thousand files, that is the difference between instant and unusable.
Partial staging
Section titled “Partial staging”The index’s most practical feature: staging some of the changes in a file.
git add -p app.pyWhat it doesWalks through each change in the file one hunk at a time, asking whether to stage it.
Why we run itIt lets you separate unrelated edits that happen to live in the same file, so each commit contains one coherent change.
Expected resultAn interactive prompt showing one hunk at a time with options: y to stage it, n to skip, s to split it into smaller hunks, q to quit, and ? for the full list.
A typical session:
@@ -12,6 +12,7 @@ def parse(payload): if not payload: return None+ print("DEBUG:", payload) return json.loads(payload)
(1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,s,e,?]?Answer n to leave a debugging line out of the commit while staging the real fix in the next hunk.
The working tree keeps both; only the index is selective.
Removing things from the index
Section titled “Removing things from the index”Three distinct operations that beginners often conflate:
| Command | Index | Working tree | Use when |
|---|---|---|---|
git restore --staged <file> | Reset from HEAD | Untouched | You staged something by mistake |
git rm --cached <file> | Entry removed entirely | File kept on disk | You want Git to stop tracking a file |
git rm <file> | Entry removed | File deleted | You want the file gone from the project |
The unstaging case is worth walking through:
echo "temporary" >> app.txtgit add app.txtgit status --shortM app.txtThe M in the left column means the index differs from HEAD. Unstage it:
git restore --staged app.txtgit status --short M app.txtThe M moved to the right column: the index now matches HEAD again, and the working tree holds
the edit. Nothing was lost — only the staging decision was reversed.
How the index becomes a commit
Section titled “How the index becomes a commit”The index is flat, but a commit needs a directory hierarchy. git commit bridges the two.
Given these index entries:
100644 0c2aa38… 0 app.txt100644 f44d1e3… 0 lib/util.txtGit builds tree objects from the leaves upward. First a tree for lib, listing util.txt and its blob
ID. Then a root tree, listing app.txt with its blob ID and lib with the ID of the tree just built.
Finally a commit object recording the root tree, the parent commit, your identity and your message.
Because the trees are constructed from IDs the index already holds, no file is read and nothing is hashed again. That is why committing a large repository is effectively instantaneous.
Lesson 11 examines the resulting objects directly.
The index when you switch branches
Section titled “The index when you switch branches”Checking out a different commit rewrites the index as well as the working tree. Git reads the target commit’s tree, replaces every index entry to match, and then updates the files on disk.
This is why a clean working tree is a precondition for switching. If your index or working tree hold changes that the checkout would overwrite, Git stops:
error: Your local changes to the following files would be overwritten by checkout: app.txtPlease commit your changes or stash them before you switch branches.Git is being careful with content it has no other copy of. Commit it, stash it, or discard it deliberately.
Inspecting the index
Section titled “Inspecting the index”| Command | Shows |
|---|---|
git ls-files | Paths in the index |
git ls-files -s | Paths with mode, object ID and stage |
git ls-files --debug | Full entries including cached filesystem metadata |
git diff --staged | Index compared to HEAD, as a diff |
git diff | Working tree compared to the index |
git status --short | Both comparisons, one line per path |
git ls-files -s is the one to reach for when you want to see the index as data rather than as
workflow.
Common misconceptions
Section titled “Common misconceptions”“The index is a list of filenames.” It is a full content listing with object IDs, modes and metadata. Filenames are the smallest part of it.
“Staging copies the file somewhere.” The content goes into the object database as a blob at
git add time. The index stores a reference. There is no separate staging directory.
“The index is temporary.” It persists on disk between commands and between sessions. Stage something today and it is still staged tomorrow.
“git add marks a file for tracking.” It records content. Tracking is a side effect of having an
index entry.
“An empty git diff means no changes.” It means the working tree matches the index. Everything
may be staged. Use git diff HEAD for changes since the last commit.
“--cached and --staged differ.” They are synonyms. --cached is the older spelling.
“You must stage everything you changed.” The whole point of the index is that you need not. Commit one coherent change at a time.
Mental Model
Section titled “Mental Model”The working tree is the draft. The index is the final copy you are preparing. The commit is filing it permanently.
You can edit the draft freely without changing the final copy. When a part of the draft is ready, you
copy it across — git add. Committing files the final copy exactly as it stands, whatever the draft
looks like at that moment.
The corollary is the thing to remember: the commit records the final copy, not the draft.
What You Learned
Section titled “What You Learned”- The index is a binary file at
.git/indexholding one entry per tracked path: mode, object ID, stage number and cached filesystem metadata. - Index, staging area and cache are three names for the same thing;
--cachedand--stagedare synonyms. - The index represents a proposed snapshot: exactly what the next commit would contain.
git addwrites a blob and points the index entry at it, capturing content at that moment.- The index exists so commits can be smaller than your working changes, so merge conflicts have somewhere to hold three versions, and so committing is fast.
git add -pstages individual hunks, separating unrelated edits in the same file.git restore --stagedreverses staging;git rm --cachedstops tracking;git rmdeletes.- Merge conflicts populate stages 1, 2 and 3;
git addcollapses them to stage 0.
Try It Yourself
Section titled “Try It Yourself”Predict each answer before running the command.
- In a disposable repository, commit a file with three lines.
- Edit line one and line three.
- Run
git add -pand stage only the first hunk. (Answerythenn.) - Run
git diffandgit diff --staged. Each should show one hunk — different ones. - Run
git ls-files -s <file>andgit ls-tree HEAD <file>. Compare the two object IDs. - Commit, then run
git show. Confirm only the first change was recorded. - Run
git status. The second change is still there, unstaged, waiting.
Step 5 is the point of the exercise: two different object IDs for one path means the index and HEAD hold two different versions of that file’s content.
Next Lesson
Section titled “Next Lesson”You now understand where the next commit comes from. The next lesson covers the reference that decides where it goes: HEAD, and the branch it points at.