What is the Staging Area?
The staging area, also known as the “index”, is Git’s intermediate layer between your working directory and the repository. It’s where you prepare and review changes before committing them to history.
From Git’s documentation:
The index is a stored version of your working tree. Truth be told, it can also contain a second, and even a third version of a working tree, which are used when merging.
The staging area is one of Git’s most powerful features. It allows you to craft precise, logical commits even when your working directory contains unrelated changes.
Why the Staging Area Exists
The staging area enables:
Selective commits - Commit only parts of your changes
Review before commit - Verify exactly what you’re about to commit
Logical grouping - Create focused, coherent commits
Partial staging - Stage specific hunks within files
Merge resolution - Track multiple versions during conflicts
The Three States of Files
In Git, your files can be in three states:
Working Directory → Staging Area → Repository
(modified) (staged) (committed)
Modified
You’ve changed the file, but haven’t staged it yet: $ git status
Changes not staged for commit:
modified: hello.py
Staged
The file is marked to go in your next commit: $ git add hello.py
$ git status
Changes to be committed:
modified: hello.py
Committed
The data is safely stored in your repository: $ git commit -m "Update greeting"
[main abc123] Update greeting
Index File Structure
The staging area is stored in .git/index, a binary file containing:
From gitdatamodel.adoc:
Each index entry has 4 fields:
The file type (regular, executable, symlink, or gitlink)
The blob ID of the file’s contents
The stage number (0 normally, 1-3 during conflicts)
The file path
Viewing the Index
You can inspect the index directly:
$ git ls-files --stage
100644 8728a858d9d21a8c78488c8b4e70e531b659141f 0 README.md
100644 665c637a360874ce43bf74018768a96d2d4219a 0 src/hello.py
This shows:
File mode (100644 = regular file)
Blob SHA-1 (the object containing file contents)
Stage number (0 = no conflict)
File path
Staging Operations
Adding Files to the Staging Area
From git-add.adoc:
Stage specific files $ git add file1.txt file2.txt
Stage all changes Stages all files in current directory and subdirectories. Stage all modified and deleted files This doesn’t stage new (untracked) files.
Stage everything (including untracked) Equivalent to git add . when run from repository root. Interactive staging Opens an interactive menu for selective staging. Patch mode Interactively stage hunks within files: Stage this hunk [y,n,q,a,d,s,e,?]?
y - stage this hunk
n - do not stage this hunk
s - split into smaller hunks
e - manually edit the hunk
Removing Files from the Staging Area
Unstage a file
$ git restore --staged file.txt
# Or using older syntax:
$ git reset HEAD file.txt
Removes the file from staging but keeps changes in working directory.
Unstage all files
$ git restore --staged .
# Or:
$ git reset HEAD
Discard staged changes
$ git restore --staged --worktree file.txt
This discards both staged and unstaged changes. Cannot be undone!
Viewing Staged Changes
Comparing States
# Show unstaged changes (working dir vs staging)
$ git diff
# Show staged changes (staging vs last commit)
$ git diff --cached
# Or:
$ git diff --staged
# Show all changes (working dir vs last commit)
$ git diff HEAD
Status Overview
$ git status
On branch main
Changes to be committed:
( use "git restore --staged <file>..." to unstage )
modified: hello.py
new file: config.yaml
Changes not staged for commit:
( use "git add <file>..." to update what will be committed )
( use "git restore <file>..." to discard changes in working directory )
modified: README.md
Untracked files:
( use "git add <file>..." to include in what will be committed )
notes.txt
This shows:
Staged : hello.py, config.yaml
Modified but not staged : README.md
Untracked : notes.txt
Partial Staging
One of Git’s most powerful features is staging parts of files:
Interactive Hunk Selection
$ git add -p hello.py
diff --git a/hello.py b/hello.py
@@ -1,4 +1,6 @@
def greet ( name ) :
- print ( f "Hello {name}" )
+ print ( f "Hello, {name}!" )
+
+def farewell ( name ) :
+ print ( f "Goodbye, {name}!" )
Stage this hunk [y,n,q,a,d,s,e,?] ? s
This splits into smaller hunks:
@@ -1,2 +1,2 @@
def greet(name):
- print(f"Hello {name}")
+ print(f"Hello, {name}!")
Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? y
@@ -3,0 +4,3 @@
+
+def farewell(name):
+ print(f"Goodbye, {name}!")
Stage this hunk [y,n,q,a,d,K,g,/,e,?]? n
Now only the greeting change is staged; the new function remains unstaged.
Use git add -p regularly to create focused commits even when working on multiple changes simultaneously.
The Index During Merges
During merge conflicts, the index can hold up to 3 versions of a file:
From read-cache-ll.h and Git documentation:
Stage 0 : Normal, unmodified entry
Stage 1 : Common ancestor version (merge base)
Stage 2 : “Ours” - current branch version
Stage 3 : “Theirs” - branch being merged
Viewing conflict stages:
$ git ls-files -u
100644 1a2b3c... 1 conflict.txt
100644 4d5e6f... 2 conflict.txt
100644 7g8h9i... 3 conflict.txt
After resolving conflicts, git add removes stages 1-3 and creates a stage 0 entry.
Staging Area Implementation
From Git’s source code:
Index Structure (read-cache-ll.h)
struct index_state {
struct cache_entry ** cache; // Array of cached entries
unsigned int cache_nr; // Number of entries
unsigned int cache_alloc; // Allocated size
struct cache_tree * cache_tree; // Tree cache for optimization
timestamp_t timestamp; // Index file timestamp
};
struct cache_entry {
unsigned int ce_mode; // File mode and type
unsigned int ce_flags; // Flags including stage
struct object_id oid; // Blob SHA-1
char name [FLEX_ARRAY]; // File path
};
Index Operations
Key functions from read-cache.c:
// Read index from disk
int read_index ( struct index_state * istate );
// Write index to disk
int write_locked_index ( struct index_state * istate ,
struct lock_file * lock ,
unsigned flags );
// Add file to index
int add_to_index ( struct index_state * istate ,
const char * path ,
struct stat * st ,
int flags );
// Remove file from index
int remove_file_from_index ( struct index_state * istate ,
const char * path );
Common Workflows
Incremental Staging
# Work on multiple features
$ vim feature1.py
$ vim feature2.py
$ vim feature3.py
# Create focused commits
$ git add feature1.py
$ git commit -m "Add feature 1"
$ git add feature2.py
$ git commit -m "Add feature 2"
$ git add feature3.py
$ git commit -m "Add feature 3"
Reviewing Before Commit
# Stage your changes
$ git add .
# Review what you're about to commit
$ git diff --cached
# Review with full context
$ git diff --cached -U5
# If satisfied, commit
$ git commit -m "Your message"
# If not, unstage and adjust
$ git restore --staged problem-file.txt
Fixing Staging Mistakes
# Staged wrong file?
$ git restore --staged wrong-file.txt
# Staged too much?
$ git restore --staged .
$ git add -p correct-file.txt
# Forgot to stage a file?
$ git add forgotten-file.txt
$ git commit --amend --no-edit
Advanced Staging Techniques
Assume Unchanged
Mark files to ignore local changes:
$ git update-index --assume-unchanged config.local
Useful for:
Local configuration files
Files you modify but don’t want to commit
This is a local setting, not shared with others. Use .gitignore for permanent ignores.
Skip Worktree
Similar but stronger than assume-unchanged:
$ git update-index --skip-worktree config.local
Difference:
--assume-unchanged: Temporary, for performance
--skip-worktree: Permanent intent to ignore
Intent to Add
Track new files without staging their content:
$ git add -N new-file.txt
$ git diff # Now shows the file
Useful for:
Seeing diffs of untracked files
Including files in stash without committing
Best Practices
Review before staging
$ git diff file.txt
$ git add file.txt
Always review changes before staging.
Stage related changes together
Group logically related changes in the same commit: $ git add feature-part1.py feature-part2.py
$ git commit -m "Add complete feature"
Use interactive staging for precision
Stage at the hunk level for clean, focused commits.
Review staged changes before committing
$ git diff --cached
$ git status
$ git commit
Double-check what’s about to be committed.
Keep the index clean
Don’t leave files staged indefinitely. Either commit or unstage: $ git commit -m "Your changes"
# Or
$ git restore --staged .
Bypassing the Staging Area
For quick commits of all changes:
$ git commit -a -m "Quick commit of all changes"
This automatically stages all tracked, modified files. Equivalent to:
$ git add -u
$ git commit -m "Quick commit of all changes"
New (untracked) files are still not included. You must git add them explicitly.
Troubleshooting
Index Corruption
If the index becomes corrupted:
# Rebuild from HEAD
$ rm .git/index
$ git reset
Staging Area Out of Sync
# Refresh the index
$ git update-index --refresh
# Verify repository integrity
$ git fsck
Further Reading
git help add - Staging files
git help restore - Unstaging and discarding changes
git help status - Viewing staging area state
git help diff - Comparing working directory, staging area, and commits