Skip to main content

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:
  1. Selective commits - Commit only parts of your changes
  2. Review before commit - Verify exactly what you’re about to commit
  3. Logical grouping - Create focused, coherent commits
  4. Partial staging - Stage specific hunks within files
  5. 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)
1

Modified

You’ve changed the file, but haven’t staged it yet:
$ git status
Changes not staged for commit:
  modified:   hello.py
2

Staged

The file is marked to go in your next commit:
$ git add hello.py
$ git status
Changes to be committed:
  modified:   hello.py
3

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:
  1. The file type (regular, executable, symlink, or gitlink)
  2. The blob ID of the file’s contents
  3. The stage number (0 normally, 1-3 during conflicts)
  4. 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

$ git add .
Stages all files in current directory and subdirectories.

Stage all modified and deleted files

$ git add -u
This doesn’t stage new (untracked) files.

Stage everything (including untracked)

$ git add -A
Equivalent to git add . when run from repository root.

Interactive staging

$ git add -i
Opens an interactive menu for selective staging.

Patch mode

$ git add -p
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

1

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

Unstage all files

$ git restore --staged .
# Or:
$ git reset HEAD
3

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

1

Review before staging

$ git diff file.txt
$ git add file.txt
Always review changes before staging.
2

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"
3

Use interactive staging for precision

$ git add -p
Stage at the hunk level for clean, focused commits.
4

Review staged changes before committing

$ git diff --cached
$ git status
$ git commit
Double-check what’s about to be committed.
5

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