Learning Objectives

By the end of this module, you will be able to:

  1. Explain what the index (staging area) is and how it fits between your working directory and the repository
  2. Use git add -p to interactively stage individual hunks of changes
  3. Split large hunks into smaller ones for granular staging
  4. Unstage files with git reset HEAD <file> and git restore --staged
  5. Use git stash to save, restore, list, and manage temporary work-in-progress
  6. Craft clean, logical commits from messy working sessions

1. What Is the Staging Area?

The staging area (also called the index) is the space between your last commit and your next commit. It's a holding area where you assemble exactly what you want in the next commit.

┌──────────────┐         ┌──────────────┐         ┌──────────────┐
│   Working    │ git add  │   Staging    │git commit│  Repository  │
│  Directory   │ ──────→  │    Area      │ ──────→  │    (HEAD)    │
│              │          │   (Index)    │          │              │
│  Your edits  │          │  Next commit │          │  History     │
└──────────────┘          └──────────────┘          └──────────────┘
       ↑                         ↑                         ↑
   git checkout --         git reset HEAD            git reset --soft
   git restore             git restore --staged
   (discard edits)         (unstage)                 (undo commit)

Why Does It Exist?

Many version control systems don't have a staging area — you commit everything you've changed. Git's staging area lets you:

  • Select which changes go into a commit (not necessarily all of them)
  • Split a day's work into multiple logical commits
  • Review exactly what you're about to commit before committing

The Index File

Physically, the staging area is a binary file at .git/index. It contains a snapshot of what the next commit will look like. When you git add a file, you're copying its current state into this index.

# The index file exists in every Git repo:
ls -la .git/index

Colors in git status

git status uses colors that map directly to the three areas:

ColorMeaningArea comparison
GreenStaged (ready to commit)Difference between index and HEAD
RedUnstaged (modified but not staged)Difference between working directory and index
UntrackedNew file Git doesn't know aboutNot in index or HEAD

git diff and the Three Areas

git diff              # Working dir vs Index (what's NOT staged)
git diff --staged     # Index vs HEAD (what IS staged, will be committed)
git diff HEAD         # Working dir vs HEAD (all changes, staged or not)
           git diff --staged        git diff
HEAD  ◄─────────────────  Index  ◄──────────────  Working Dir
          (what's staged)          (what's not staged)

              git diff HEAD
HEAD  ◄──────────────────────────────────────────  Working Dir
                    (everything)

2. git add -p — Interactive Patch Staging

git add -p (or git add --patch) is the most powerful staging tool. Instead of staging entire files, it walks you through each hunk (block of changes) and lets you decide individually: stage this one or skip it.

Basic Usage

git add -p              # Patch-add tracked files
git add -p .            # Same, explicit current directory
git add -p file.py      # Patch-add a specific file

The Interactive Prompt

For each hunk, Git shows you the diff and asks:

@@ -1,5 +1,6 @@
 def greet(name):
-    return f"Hello, {name}"
+    return f"Hello, {name}!"
+    # Added exclamation mark

 def farewell(name):
     return f"Goodbye, {name}"
Stage this hunk [y,n,q,a,d,s,e,?]?

The Commands

KeyAction
yYes — stage this hunk
nNo — skip this hunk
qQuit — don't stage this hunk or any remaining hunks
aAll — stage this hunk and all remaining hunks in this file
dDon't — don't stage this hunk or any remaining hunks in this file
sSplit — try to split this hunk into smaller hunks
eEdit — manually edit the hunk (advanced)
?Help — show this list

Splitting Hunks

If Git groups changes together that you want to stage separately, press s to split:

Stage this hunk [y,n,q,a,d,s,e,?]? s
Split into 3 hunks.
@@ -1,3 +1,3 @@
-old line 1
+new line 1
 unchanged line
Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?

Git splits the hunk at any unchanged line between changed areas. If the changes are adjacent (no unchanged lines between them), s won't be able to split further — use e for manual editing in that case.

The e (Edit) Option

When s can't split small enough, e opens the hunk in your editor. You see the diff and can:

  • Delete lines starting with + to exclude them from staging
  • Change - lines to a space ( ) to keep the original line (don't stage the removal)
  • Never modify context lines (lines starting with a space)

This is advanced but powerful for surgical staging.

Handling New (Untracked) Files

git add -p only works on tracked files. New files are untracked and won't appear in the patch session. To include them:

# Mark new files as "intent to add" — tells Git to track them
git add -N .
 
# Now git add -p will include new files
git add -p .

The -N (or --intent-to-add) flag registers the file in the index as empty, so git add -p can show its contents as a "new hunk" to stage.


3. Unstaging and Discarding Changes

Unstaging Files

Remove files from the staging area without affecting your working directory:

# Traditional way:
git reset HEAD file.txt
 
# Modern way (Git 2.23+):
git restore --staged file.txt
 
# Unstage everything:
git reset HEAD

Discarding Working Directory Changes

Throw away edits in your working directory, reverting a file to what's in the index:

# Traditional way:
git checkout -- file.txt
 
# Modern way (Git 2.23+):
git restore file.txt

Warning: Discarding working directory changes is irreversible for uncommitted work. The edits are gone.

The Complete Undo Matrix

What you want to undoCommand
Unstage a file (keep edits)git restore --staged file.txt
Discard edits to a file (revert to index)git restore file.txt
Unstage AND discard editsgit restore --staged --worktree file.txt
Unstage everythinggit reset HEAD
Discard all edits (match index)git restore .
Discard all edits (match HEAD)git checkout -- .

4. Crafting Commits from Chaos

The real power of the staging area emerges when you combine git add -p with a git reset workflow. Here's the professional pattern:

The Workflow

1. Work messily — edit many files, mix features and fixes
2. git reset origin/main — collapse all commits, keep changes
3. git add -p — interactively stage logical groups
4. git commit — commit the first logical unit
5. Repeat 3-4 until all changes are committed
6. git push --force-with-lease — replace messy history with clean one

Example: Three Concerns, One Session

You modified 6 files during a coding session. The changes fall into three categories:

  • Dependency version bumps (build files)
  • New feature (source files)
  • Refactoring (source files)
# 1. Reset all WIP commits but keep changes
git reset origin/main
 
# 2. Stage only the dependency changes
git add -p
# Answer y for version changes, n for everything else
 
git commit -m "Bump dependency versions"
 
# 3. Stage only the feature changes
git add -p
# Answer y for feature code, n for refactoring
 
git commit -m "Add user search feature"
 
# 4. Stage the remaining refactoring
git add -A
git commit -m "Refactor query builder"

Result: three clean, logical commits from one messy working session.


5. git stash — Temporary Storage

git stash saves your uncommitted changes to a stack and reverts your working directory to a clean state. It's designed for context switches — when you need to quickly work on something else.

Core Operations

git stash                    # Stash tracked, modified files
git stash -u                 # Stash including untracked files
git stash push -m "message"  # Stash with a descriptive message
git stash list               # Show all stashes
git stash pop                # Apply top stash and remove it
git stash apply              # Apply top stash but keep it
git stash drop               # Remove top stash without applying
git stash clear              # Remove ALL stashes
git stash show               # Show diff summary of top stash
git stash show -p            # Show full diff of top stash

Stash Is a Stack

Stashes are stored as a stack (LIFO — last in, first out):

git stash list:
  stash@{0}: WIP on feature: abc1234 Latest change
  stash@{1}: On main: def5678 Quick experiment
  stash@{2}: WIP on feature: ghi9012 Debug session

  ← stash@{0} is the most recent (top of stack)
  ← stash@{2} is the oldest

You can access any stash by index:

git stash apply stash@{2}    # Apply a specific stash
git stash drop stash@{1}     # Drop a specific stash
git stash show stash@{2} -p  # Show a specific stash's diff

git stash push vs git stash (Legacy)

The modern syntax uses push:

git stash push -m "work on auth"           # Stash everything with message
git stash push -m "just models" model.py   # Stash specific files
git stash push --keep-index                # Stash unstaged only, keep staged

The bare git stash (without push) still works for basic stashing.

git stash -p — Partial Stash

Just like git add -p, you can interactively choose which hunks to stash:

git stash push -p -m "just the debug code"

Git walks through each hunk and asks whether to stash it. This lets you stash only part of your changes while keeping the rest in your working directory.

Including Untracked Files

By default, git stash only saves tracked files. New (untracked) files are left behind:

git stash              # Tracked files only — new files remain
git stash -u           # Include untracked files
git stash --all        # Include untracked AND ignored files (rarely needed)

Recommendation: Use git stash -u as your default. New files are usually part of the work you want to stash.

Recovering a Stash as a Branch

If applying a stash causes conflicts (because the code changed since you stashed), create a branch from the stash instead:

git stash branch temp-branch stash@{0}

This creates a new branch starting from the commit where the stash was originally created, applies the stash, and drops it from the stash list. The stash is almost guaranteed to apply cleanly because the codebase matches the state when you stashed.

Stash Internals

Stashes are actually commits — they appear in git log --all as a special refs/stash reference. They're local-only and are never pushed to a remote.


6. git stash and Rebase

Rebase requires a clean working directory. If you have uncommitted changes, you have two options:

Option 1: Manual Stash

git stash -u
git rebase main
git stash pop

Option 2: --autostash

git rebase main --autostash

Git automatically stashes before the rebase and pops after. You can make this the default:

git config --global rebase.autoStash true

Caveat: If the stash pop causes conflicts after the rebase, you'll need to resolve them manually. This is uncommon but possible.


Command Reference

CommandDescription
git add -pInteractively stage hunks
git add -p <file>Interactively stage hunks from a specific file
git add -N <file>Mark untracked file as "intent to add"
git diffShow unstaged changes (working dir vs index)
git diff --stagedShow staged changes (index vs HEAD)
git diff HEADShow all changes (working dir vs HEAD)
git reset HEAD <file>Unstage a file
git restore --staged <file>Unstage a file (modern)
git restore <file>Discard working directory changes (modern)
git checkout -- <file>Discard working directory changes (traditional)
git stashStash tracked modified files
git stash -uStash including untracked files
git stash push -m "msg"Stash with a message
git stash push -pInteractively choose hunks to stash
git stash listList all stashes
git stash popApply and remove top stash
git stash applyApply top stash (keep it)
git stash apply stash@{N}Apply a specific stash
git stash drop stash@{N}Remove a specific stash
git stash show -pShow full diff of top stash
git stash branch <name>Create branch from stash
git stash clearRemove all stashes

Hands-On Lab: Surgical Commits and Context Switching

Setup

mkdir staging-lab && cd staging-lab
git init
 
cat > app.py << 'EOF'
def add(a, b):
    return a + b
 
def subtract(a, b):
    return a - b
 
def multiply(a, b):
    return a * b
 
def divide(a, b):
    return a / b
EOF
 
git add app.py
git commit -m "Initial commit: basic calculator"

Part A: Interactive Staging with git add -p

Step 1 — Make multiple unrelated changes to one file

cat > app.py << 'EOF'
def add(a, b):
    """Add two numbers."""
    return a + b
 
def subtract(a, b):
    """Subtract b from a."""
    return a - b
 
def multiply(a, b):
    return a * b
 
def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b
EOF

You've made two types of changes:

  • Added docstrings to add and subtract (documentation)
  • Added zero-division check to divide (bug fix)

Step 2 — Use git add -p to stage only the bug fix

git add -p app.py

For each hunk:

  • The docstring hunks → press n (skip)
  • The divide zero-check hunk → press y (stage)
git status

Checkpoint: app.py appears in BOTH the staged and unstaged sections. The staged changes contain only the divide fix. The unstaged changes contain only the docstrings.

Step 3 — Verify with git diff

git diff --staged    # Shows only the divide fix
git diff             # Shows only the docstrings

Step 4 — Commit the bug fix

git commit -m "Fix: add zero-division check to divide()"

Step 5 — Commit the documentation

git add app.py
git commit -m "Add docstrings to add() and subtract()"
git log --oneline

Checkpoint: Two clean, focused commits. Each does one thing.

Part B: Splitting Hunks

Step 6 — Make changes that Git groups into one hunk

cat > app.py << 'EOF'
def add(a, b):
    """Add two numbers."""
    return int(a) + int(b)
 
def subtract(a, b):
    """Subtract b from a."""
    return int(a) - int(b)
 
def multiply(a, b):
    return a * b
 
def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b
EOF

Step 7 — Try git add -p and split

git add -p app.py

When presented with the hunk, press s to split it. Stage only the first change (add function), skip the second (subtract function).

git diff --staged
git diff

Checkpoint: Only the add function change is staged. The subtract change is unstaged.

git commit -m "Add type coercion to add()"
git add app.py
git commit -m "Add type coercion to subtract()"

Part C: Stashing for Context Switches

Step 8 — Start working on a feature, then get interrupted

echo "def power(a, b):" >> app.py
echo "    return a ** b" >> app.py
echo "# TODO: add logging" >> app.py
git status

Step 9 — Stash your work

git stash push -u -m "WIP: power function"
git status
cat app.py

Checkpoint: Working directory is clean. app.py doesn't contain the power function.

Step 10 — Do the "urgent" work

echo "" >> app.py
echo "def modulo(a, b):" >> app.py
echo "    return a % b" >> app.py
git add app.py
git commit -m "Add modulo function"

Step 11 — Restore your stashed work

git stash list
git stash pop
cat app.py
git status

Checkpoint: The power function is back. The modulo function is also there (from the commit). The stash list is empty (pop removes the stash after applying).

git add app.py
git commit -m "Add power function"

Part D: Multiple Stashes

Step 12 — Create multiple stashes and manage them

echo "# Config A" > config_a.txt
git stash push -u -m "Config A experiment"
 
echo "# Config B" > config_b.txt
git stash push -u -m "Config B experiment"
 
git stash list

Checkpoint: Two stashes listed. Config B is at stash@{0}, Config A is at stash@{1}.

Step 13 — Apply a specific stash

git stash apply stash@{1}    # Apply Config A (not the top)
ls
git stash drop stash@{1}     # Drop Config A from the list
git stash list               # Only Config B remains

Step 14 — Stash as a branch

git stash branch config-branch stash@{0}
git status
ls
git stash list

Checkpoint: You're on a new branch config-branch. config_b.txt exists. The stash list is empty.

Cleanup

git switch main
git branch -D config-branch
cd ..
rm -rf staging-lab

Challenge

Create a file with 20+ lines. Make 6 different changes across the file (mix of bug fixes, features, and documentation). Using only git add -p, create exactly 3 commits:

  1. "Fix bugs" — containing only the bug fix hunks
  2. "Add feature" — containing only the new feature hunks
  3. "Update docs" — containing only the documentation hunks

Common Pitfalls & Troubleshooting

PitfallSymptomFix
Using git add -A when you meant git add -pAll changes staged at once, ruining your careful staginggit reset HEAD to unstage everything, then redo with -p
git add -p doesn't show new filesUntracked files are invisible to patch modegit add -N . first to mark them as intent-to-add
Stashing doesn't include new filesNew files left in working directory after stashUse git stash -u to include untracked files
Stash pop causes conflictsChanges conflict with work done since stashinggit stash branch <name> to apply on the original base
Forgetting stashes existAccumulated stashes never appliedRun git stash list periodically; set a reminder
Accidentally discarding editsgit checkout -- . is irreversible for uncommitted workNo recovery — commit or stash before running destructive commands
Confusing git diff and git diff --stagedNot sure what's staged vs unstagedgit diff = unstaged, git diff --staged = staged, git diff HEAD = everything
Using git stash --allStashes git-ignored files (build artifacts, etc.)Use git stash -u instead — includes untracked but not ignored

Pro Tips

  1. Make git add -p your default habit. Even if you're staging everything, git add -p . forces you to review each change before committing. It catches debugging code, accidental edits, and unrelated changes.

  2. Use git add -N before git add -p to include new files in your patch session:

    git add -N . && git add -p .
  3. Use git diff --staged before every commit to verify exactly what you're committing:

    git diff --staged
    git commit -m "message"
  4. Give stashes descriptive messagesgit stash push -m "WIP: auth flow" is infinitely more useful than the default WIP on main: abc1234.

  5. Don't use stash as long-term storage. Stashes are local, unnamed, and easy to forget. If you need to save work for more than a few hours, commit it to a branch instead.

  6. The --keep-index flag lets you test what you've staged:

    git stash push --keep-index
    # Run tests — only staged changes are in the working dir
    # If tests pass, your staged commit is good
    git stash pop
  7. git commit -v (verbose) shows the diff in the commit message editor, so you can review changes while writing the message. It's the equivalent of a final git diff --staged check.


Quiz / Self-Assessment

Q1: What is the staging area and why does Git have one?

Answer

The staging area (index) is a snapshot of what will go into the next commit. It sits between the working directory and the repository. Git has it so you can selectively choose which changes to include in a commit rather than committing everything at once. This allows you to create focused, logical commits from a messy working session.

Q2: What's the difference between git diff, git diff --staged, and git diff HEAD?

Answer
  • git diff — compares the working directory to the index (shows unstaged changes)
  • git diff --staged — compares the index to HEAD (shows what will be committed)
  • git diff HEAD — compares the working directory to HEAD (shows all changes, staged or not)

Q3: You run git add -p and one hunk contains two unrelated changes. What do you do?

Answer

Press s to split the hunk. Git will divide it into smaller hunks at any unchanged line between changed regions. If s can't split it (the changes are adjacent with no unchanged lines between them), press e to manually edit the hunk in your text editor.

Q4: Why doesn't git add -p show new (untracked) files?

Answer

git add -p operates on the diff between the working directory and the index. Untracked files have no entry in the index, so there's no diff to show. To include new files, run git add -N . first, which registers them in the index as empty. Then git add -p can show their content as new hunks.

Q5: What's the difference between git stash pop and git stash apply?

Answer

Both apply the top stash to your working directory. The difference: pop removes the stash from the stash list after applying it successfully, while apply keeps the stash in the list. Use apply when you want to apply the same stash to multiple branches or keep it as a backup.

Q6: How do you stash only some of your changes (not all of them)?

Answer

Use git stash push -p for interactive hunk selection (like git add -p but for stashing). Or stash specific files: git stash push file1.py file2.py. You can also combine: git stash push -p -m "just debug code" .

Q7: What does git stash -u do differently from plain git stash?

Answer

git stash only stashes tracked, modified files. New (untracked) files are left in the working directory. git stash -u (or --include-untracked) also stashes untracked files, giving you a truly clean working directory.

Q8: You stashed some work a week ago. Now git stash pop causes conflicts because the code has changed. What's the safest recovery?

Answer

Use git stash branch <name> stash@{N}. This creates a new branch from the commit where the stash was originally created, applies the stash there (where it's guaranteed to apply cleanly), and removes it from the stash list. You can then merge or rebase this branch to integrate the changes.

Q9: Describe the workflow for turning a messy day of coding into three clean commits.

Answer
  1. git reset origin/main — collapse all WIP commits, keeping changes unstaged
  2. git add -p — interactively stage only the hunks for the first logical change
  3. git commit -m "First logical change"
  4. git add -p — stage hunks for the second logical change
  5. git commit -m "Second logical change"
  6. git add -A && git commit -m "Third logical change" — commit everything remaining
  7. git push --force-with-lease — replace the messy history with clean commits

Q10: In git status, a file appears in both the green (staged) and red (unstaged) sections. How is this possible?

Answer

This happens when you stage part of a file's changes (using git add -p) but not all of them. The staged hunks appear in green (difference between index and HEAD), while the remaining unstaged hunks appear in red (difference between working directory and index). The file exists in three different states simultaneously: HEAD version, index version (partially updated), and working directory version (fully updated).