The Git Index (Staging Area)
The Git index — also called the staging area or cache — is the intermediary between your working directory and the Git object store. It is a binary file at /.git/index that records exactly what the next commit will look like. Understanding the index clarifies why staging exists, what git add and git reset actually do, and how Git detects changes so quickly.
What the Index Stores
For each tracked file, the index stores:
Filename — the full path relative to the repository root
Mode — file permissions (100644 for regular, 100755 for executable)
Blob SHA — the hash of the file content at the staged version
Stat information — inode, file size, mtime, ctime (for fast change detection)
Stage number — 0 for normal, 1/2/3 during merge conflicts (base/ours/theirs)
Inspecting the Index with git ls-files
List files tracked by the index
# Show all files in the index git ls-files # Show with stage number and blob hash (--stage) git ls-files --stage
git ls-files --stage output
100644 8f14e0b9bbd2c8fc72c99b35d0a5b61e07b19b3c 0 README.md 100644 1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b 0 package.json 100644 9e2b0f1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a 0 src/index.ts 100644 6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f 0 src/auth.ts Format: <mode> <blob-hash> <stage-number> <filename> Stage 0 = normal (no conflict)
Other useful ls-files flags
# Show only modified files (staged vs working tree) git ls-files --modified # Show untracked files git ls-files --others # Show deleted files git ls-files --deleted # Show files with their stat information (verbose) git ls-files --debug src/index.ts
The Three-Tree Model
Git operates on three distinct "trees" simultaneously. Understanding all three is the key to understanding git diff, git add, git reset, and git checkout:
Three-tree model diagram
Working Directory Index (Staging Area) HEAD (last commit)
───────────────── ──────────────────── ──────────────────
Your actual files Proposed next commit Current committed state
on disk (.git/index) (a commit object)
←───── git restore ──────← ←──── git restore --staged ────←
─────── git add ──────→ ──────── git commit ──────────→
git diff git diff --staged (compare from HEAD)
(working vs index) (index vs HEAD)
git status shows both differences at onceSeeing all three trees in action
# Edit a file echo "new feature" >> src/index.ts # Check differences between all three trees: git diff # working directory vs index (unstaged changes) git diff --staged # index vs HEAD (staged changes) git diff HEAD # working directory vs HEAD (all changes) # Stage the change git add src/index.ts # Now the change moved from "unstaged" to "staged": git diff # (empty — working dir matches index) git diff --staged # (shows the change — index differs from HEAD) git diff HEAD # (same as --staged — HEAD is behind)
What git add Does to the Index
git add in detail
# Before add: check the blob hash in the index for src/auth.ts git ls-files --stage src/auth.ts # 100644 6e7f8a9b0c1d... 0 src/auth.ts # Edit the file echo "// new comment" >> src/auth.ts # After editing but before add: working dir differs from index git diff src/auth.ts # shows the new comment # Run git add git add src/auth.ts # After add: index now has the new blob hash git ls-files --stage src/auth.ts # 100644 b2c3d4e5f6a7... 0 src/auth.ts ← new hash! # The blob b2c3d4e5... is now in .git/objects/ git cat-file -p b2c3d4e5f6a7 # shows the updated file content
Merge Conflicts and the Index Stages
During a merge conflict, the index stores three versions of the conflicting file — one for each parent plus the common ancestor. These are called stages 1, 2, and 3:
Stage Number | Name | Meaning |
|---|---|---|
0 | Normal | No conflict — single version |
1 | Base (ancestor) | The common ancestor version before the branches diverged |
2 | Ours | The version from the branch we are merging INTO (HEAD) |
3 | Theirs | The version from the branch being merged in |
Inspecting conflict stages
# During a merge conflict, ls-files -u shows all stages git ls-files -u # 100644 1a2b3c4d... 1 src/auth.ts ← stage 1: base # 100644 9e2b0f1c... 2 src/auth.ts ← stage 2: ours (HEAD) # 100644 6e7f8a9b... 3 src/auth.ts ← stage 3: theirs # View each version individually git cat-file -p :1:src/auth.ts # base version git cat-file -p :2:src/auth.ts # our version git cat-file -p :3:src/auth.ts # their version # Accept "theirs" for a file (resolve by taking their version) git checkout --theirs src/auth.ts git add src/auth.ts # mark as resolved (stage returns to 0)
Reset Modes and the Index
The three git reset modes differ in which trees they update:
Reset Mode | Moves HEAD? | Updates Index? | Updates Working Dir? |
|---|---|---|---|
--soft | Yes | No | No |
--mixed (default) | Yes | Yes | No |
--hard | Yes | Yes | Yes |
Reset modes in practice
# --soft: undo commit, keep changes staged git reset --soft HEAD~1 git status # changes are staged, ready to re-commit # --mixed (default): undo commit AND unstage, keep working dir git reset HEAD~1 git status # changes are in working dir, not staged # --hard: undo commit AND discard all changes git reset --hard HEAD~1 git status # working dir is clean, changes are gone
Partial Staging: Adding Hunks
Stage only part of a file's changes
# Interactive hunk-by-hunk staging git add -p src/auth.ts # Git shows each changed hunk and asks: # Stage this hunk [y,n,q,a,d,/,e,?]? # y = yes, stage this hunk # n = no, skip this hunk # s = split into smaller hunks # e = manually edit the hunk
The Index as an Optimization
The index is also a performance optimization. Git stores the stat info (mtime, inode, size) alongside each entry. On git status, Git first checks if the stat has changed. If not, it skips hashing entirely. On a repository with 100,000 files, this makes git status run in milliseconds instead of seconds.
Rebuild the stat cache (fix false positives)
# If git status shows everything as modified after a filesystem operation: git update-index --refresh # Force re-stat every file git status # after refresh, false positives should be gone