Learning Objectives
By the end of this module, you will be able to:
- Explain what the index (staging area) is and how it fits between your working directory and the repository
- Use
git add -pto interactively stage individual hunks of changes - Split large hunks into smaller ones for granular staging
- Unstage files with
git reset HEAD <file>andgit restore --staged - Use
git stashto save, restore, list, and manage temporary work-in-progress - 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/indexColors in git status
git status uses colors that map directly to the three areas:
| Color | Meaning | Area comparison |
|---|---|---|
| Green | Staged (ready to commit) | Difference between index and HEAD |
| Red | Unstaged (modified but not staged) | Difference between working directory and index |
| Untracked | New file Git doesn't know about | Not 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 fileThe 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
| Key | Action |
|---|---|
y | Yes — stage this hunk |
n | No — skip this hunk |
q | Quit — don't stage this hunk or any remaining hunks |
a | All — stage this hunk and all remaining hunks in this file |
d | Don't — don't stage this hunk or any remaining hunks in this file |
s | Split — try to split this hunk into smaller hunks |
e | Edit — 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 HEADDiscarding 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.txtWarning: Discarding working directory changes is irreversible for uncommitted work. The edits are gone.
The Complete Undo Matrix
| What you want to undo | Command |
|---|---|
| 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 edits | git restore --staged --worktree file.txt |
| Unstage everything | git 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 stashStash 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 diffgit 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 stagedThe 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 -uas 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 popOption 2: --autostash
git rebase main --autostashGit automatically stashes before the rebase and pops after. You can make this the default:
git config --global rebase.autoStash trueCaveat: If the stash pop causes conflicts after the rebase, you'll need to resolve them manually. This is uncommon but possible.
Command Reference
| Command | Description |
|---|---|
git add -p | Interactively 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 diff | Show unstaged changes (working dir vs index) |
git diff --staged | Show staged changes (index vs HEAD) |
git diff HEAD | Show 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 stash | Stash tracked modified files |
git stash -u | Stash including untracked files |
git stash push -m "msg" | Stash with a message |
git stash push -p | Interactively choose hunks to stash |
git stash list | List all stashes |
git stash pop | Apply and remove top stash |
git stash apply | Apply 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 -p | Show full diff of top stash |
git stash branch <name> | Create branch from stash |
git stash clear | Remove 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
EOFYou've made two types of changes:
- Added docstrings to
addandsubtract(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.pyFor each hunk:
- The docstring hunks → press
n(skip) - The divide zero-check hunk → press
y(stage)
git statusCheckpoint:
app.pyappears 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 docstringsStep 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 --onelineCheckpoint: 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
EOFStep 7 — Try git add -p and split
git add -p app.pyWhen 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 diffCheckpoint: Only the
addfunction change is staged. Thesubtractchange 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 statusStep 9 — Stash your work
git stash push -u -m "WIP: power function"
git status
cat app.pyCheckpoint: Working directory is clean.
app.pydoesn'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 statusCheckpoint: 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 listCheckpoint: Two stashes listed. Config B is at
stash@{0}, Config A is atstash@{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 remainsStep 14 — Stash as a branch
git stash branch config-branch stash@{0}
git status
ls
git stash listCheckpoint: You're on a new branch
config-branch.config_b.txtexists. The stash list is empty.
Cleanup
git switch main
git branch -D config-branch
cd ..
rm -rf staging-labChallenge
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:
- "Fix bugs" — containing only the bug fix hunks
- "Add feature" — containing only the new feature hunks
- "Update docs" — containing only the documentation hunks
Common Pitfalls & Troubleshooting
| Pitfall | Symptom | Fix |
|---|---|---|
Using git add -A when you meant git add -p | All changes staged at once, ruining your careful staging | git reset HEAD to unstage everything, then redo with -p |
git add -p doesn't show new files | Untracked files are invisible to patch mode | git add -N . first to mark them as intent-to-add |
| Stashing doesn't include new files | New files left in working directory after stash | Use git stash -u to include untracked files |
| Stash pop causes conflicts | Changes conflict with work done since stashing | git stash branch <name> to apply on the original base |
| Forgetting stashes exist | Accumulated stashes never applied | Run git stash list periodically; set a reminder |
| Accidentally discarding edits | git checkout -- . is irreversible for uncommitted work | No recovery — commit or stash before running destructive commands |
Confusing git diff and git diff --staged | Not sure what's staged vs unstaged | git diff = unstaged, git diff --staged = staged, git diff HEAD = everything |
Using git stash --all | Stashes git-ignored files (build artifacts, etc.) | Use git stash -u instead — includes untracked but not ignored |
Pro Tips
-
Make
git add -pyour 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. -
Use
git add -Nbeforegit add -pto include new files in your patch session:git add -N . && git add -p . -
Use
git diff --stagedbefore every commit to verify exactly what you're committing:git diff --staged git commit -m "message" -
Give stashes descriptive messages —
git stash push -m "WIP: auth flow"is infinitely more useful than the defaultWIP on main: abc1234. -
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.
-
The
--keep-indexflag 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 -
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 finalgit diff --stagedcheck.
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
git reset origin/main— collapse all WIP commits, keeping changes unstagedgit add -p— interactively stage only the hunks for the first logical changegit commit -m "First logical change"git add -p— stage hunks for the second logical changegit commit -m "Second logical change"git add -A && git commit -m "Third logical change"— commit everything remaininggit 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).