Managing Multiple Stashes
A single git stash command is easy to reason about, but real work rarely stays that tidy. You stash a half-finished feature to fix an urgent bug, then stash again to review a teammate’s branch, and before long you have three or four stashes sitting around with no idea which is which. Git doesn’t just support one stash at a time — it keeps every stashed change in an ordered stack, and knowing how that stack is built, referenced, and cleaned up is the difference between stashing being a lifesaver and stashing being how you lose a day of work. This lesson covers the stash stack in depth: how entries are stored as real commit objects, how to list and inspect them, how to apply the right one to the right branch, and how to recover when an apply goes wrong.
Overview: How the Stash Stack Works
When you run git stash (shorthand for git stash push), Git doesn’t just remember your changes in some vague sense — it builds real commit objects out of them and stores a reference to the newest one at .git/refs/stash. A typical stash actually creates up to three commits: one representing your working-tree changes, one representing what was staged in the index at the time, and — only if you pass -u or -a — one representing untracked or ignored files. These are stitched together as a single stash commit with multiple parents, with the base parent being whatever commit HEAD pointed to when you stashed. This is why a stash can be applied cleanly to a different branch than the one it was created on: it’s just a commit with a known parent, and Git can diff it against your current tree like any other commit.
Because refs/stash can only point at one commit at a time, Git uses the reflog of that single reference to keep a history of every stash you’ve ever pushed, most recent first. That reflog is what git stash list reads, and it’s why entries are numbered stash@{0}, stash@{1}, stash@{2}, and so on — stash@{0} is always the most recently created stash, and the numbers shift as you add or remove entries. Stash commits sit outside your normal branch history: they don’t show up in git log, they don’t belong to any branch, and nothing else points to them except that reflog. That also makes them more fragile than an ordinary commit — running git stash clear, or letting the reflog expire, can make that history disappear.
Because the stack is LIFO (last in, first out) by default, plain git stash pop or git stash apply with no argument always targets stash@{0} — whatever you stashed most recently, regardless of which branch you’re currently on. When you’re juggling several stashes from different branches or tasks, relying on that default is exactly how the wrong changes end up applied to the wrong branch. Managing multiple stashes well means always addressing entries explicitly by index or message, and treating the stack as a small, temporary shelf — not a place to archive work indefinitely.
Syntax
The core subcommands for working with more than one stash:
git stash push [-m "<message>"] [-u] [-a] [-- <pathspec>]
git stash list
git stash show [-p] stash@{<n>}
git stash apply stash@{<n>}
git stash pop stash@{<n>}
git stash drop stash@{<n>}
git stash branch <new-branch-name> stash@{<n>}
git stash clear
| Command | What it does |
|---|---|
git stash push -m "<message>" |
Stashes tracked changes with a human-readable label instead of the default WIP on <branch>: <sha> <subject> message. |
-u / --include-untracked |
Also stashes new, untracked files (still respects .gitignore). |
-a / --all |
Stashes untracked and ignored files too. |
-- <pathspec> |
Limits the stash to specific files or directories, leaving the rest of your changes in place. |
git stash list |
Prints every entry in the stack, newest first, as stash@{n}: <label>. |
git stash show [-p] stash@{n} |
Shows a diffstat (or, with -p, the full patch) for one specific stash without applying it. |
git stash apply stash@{n} |
Re-applies the given stash’s changes to the working tree, but leaves it on the stack. |
git stash pop stash@{n} |
Same as apply, then removes the entry from the stack — but only if the apply succeeded without conflicts. |
git stash drop stash@{n} |
Deletes one specific stash entry without applying it. |
git stash branch <name> stash@{n} |
Creates a new branch from the commit the stash was based on, checks it out, applies the stash, and drops it if that succeeds. |
git stash clear |
Deletes every stash entry at once. Irreversible in the normal Git workflow. |
If you omit stash@{n} from show, apply, pop, or drop, Git defaults to stash@{0} — the most recent entry.
Examples
Example 1: Building up multiple stashes across branches
Suppose you’re mid-way through two separate features and need to switch context on both:
git switch -c feature/login-page
echo "// TODO: add password strength meter" >> login.js
git add login.js
git stash push -m "WIP: login form validation"
git switch main
git switch -c feature/dashboard-widgets
echo "// TODO: fix widget resize handle" >> widget.js
git add widget.js
git stash push -m "WIP: dashboard widget layout"
git stash list
Output:
stash@{0}: On feature/dashboard-widgets: WIP: dashboard widget layout
stash@{1}: On feature/login-page: WIP: login form validation
Two unrelated pieces of work are now safely shelved. Note that stash@{0} is always the newest — the dashboard-widgets stash was pushed second, so the login-page one from earlier is now stash@{1}. Without the -m message, both entries would show a far less useful default like WIP on feature/dashboard-widgets: 4a3f2c1 Add widget scaffolding, making them hard to tell apart later.
Example 2: Inspecting a stash before touching anything
Before applying or dropping anything, look at what’s actually inside each entry:
git stash show stash@{1}
git stash show -p stash@{1}
Output:
login.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/login.js b/login.js
index e69de29..4b825dc 100644
--- a/login.js
+++ b/login.js
@@ -0,0 +1 @@
+// TODO: add password strength meter
Plain show gives a quick diffstat; adding -p prints the full patch, exactly like git show would for a normal commit — because under the hood, that’s exactly what it is. Inspecting first means you never have to guess which numbered entry is the one you actually want.
Example 3: Applying an older stash and cleaning up
git switch feature/login-page
git stash apply stash@{1}
git status
git stash drop stash@{1}
Output:
On branch feature/login-page
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
modified: login.js
Dropped stash@{1} (b2f6a91c8de4f27a5b9c0e1d3f4a5b6c7d8e9f0a)
apply restores the changes without touching the stack, which is safer than pop when you want to confirm everything looks right first. Only after checking git status and the diff do we explicitly drop the entry. Note that we referenced it as stash@{1} the whole time rather than stash@{0} — the dashboard-widgets stash is still sitting untouched on top of the stack.
Example 4: Recovering from a conflicting apply with git stash branch
Sometimes a branch has moved on since you stashed, and applying directly would produce conflicts. git stash branch sidesteps that by rebuilding the exact branch state the stash expects:
git stash list
git stash branch fix/widget-resize-handle stash@{0}
Output:
stash@{0}: On feature/dashboard-widgets: WIP: dashboard widget layout
Switched to a new branch 'fix/widget-resize-handle'
On branch fix/widget-resize-handle
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
modified: widget.js
Dropped stash@{0} (f47ac10b58cc4372a5670e02b2c3d479)
git stash branch creates a new branch starting from the commit the stash was originally based on, switches to it, applies the stash’s changes, and — only if that apply succeeds cleanly — drops the stash automatically. This is the safest option whenever applying directly to your current branch would conflict, since you get a clean base to work from instead of fighting merge conflicts on top of unrelated new commits.
How It Works Step by Step
When you run git stash push:
- Git looks at the current state of the index and the working tree relative to
HEAD. - It creates a commit object for the working-tree state, whose parent is a second new commit representing the index (staged) state, whose parent in turn is the commit
HEADcurrently points to. - If
-uor-awas passed, a third commit is created holding untracked (and, with-a, ignored) files, also attached as a parent. refs/stashis updated to point at this new combined stash commit, and an entry is appended to that reference’s reflog — this is what makes itstash@{0}.- Your working tree and index are reset back to match
HEAD, so it looks like the changes never happened.
When you later run git stash apply stash@{n}:
- Git performs a three-way merge between the stash’s base commit, its working-tree commit, and your current
HEAD. - If that merge is clean, the resulting changes are written into your working tree (and index, for changes that were staged when stashed).
- If there’s a conflict, Git leaves conflict markers in the affected files and stops — the stash entry is not removed from the stack either way unless you used
popand it succeeded.
git stash drop simply removes one entry from the refs/stash reflog. Because the remaining entries are renumbered from newest to oldest, dropping stash@{0} means the old stash@{1} immediately becomes the new stash@{0} — the underlying commits themselves don’t move, but their index labels do.
Common Mistakes
Mistake 1: Forgetting that indices shift after a drop or pop
With several stashes on the stack, it’s easy to assume stash@{2} still refers to the same entry after you’ve dropped something earlier in the list:
git stash list
git stash drop stash@{0}
git stash list
Output:
stash@{0}: On feature/dashboard-widgets: WIP: dashboard widget layout
stash@{1}: On feature/login-page: WIP: login form validation
stash@{2}: On main: WIP: hotfix for null pointer
Dropped stash@{0} (a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0)
stash@{0}: On feature/login-page: WIP: login form validation
stash@{1}: On main: WIP: hotfix for null pointer
What used to be stash@{1} is now stash@{0}. If a script or muscle-memory habit references stashes by a fixed index without re-running git stash list first, it will silently operate on the wrong entry. Always re-list before acting on an index.
Mistake 2: Assuming git stash push grabs new files too
echo "SECRET_KEY=devkey123" >> .env.local
git stash push -m "WIP: refactor settings loader"
git status
Output:
On branch feature/dashboard-widgets
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.local
nothing added to commit but untracked files present
The stash “succeeded,” but .env.local was never touched because it’s untracked and -u wasn’t passed. Someone expecting a clean working tree can be surprised to find a leftover file still sitting there — or worse, assume the new file was safely stashed when it wasn’t. Use git stash push -u -m "<message>" whenever new files are part of the work you’re setting aside.
Mistake 3: Treating git stash clear as harmless cleanup
git stash clear
This deletes every entry in the stack at once, not just ones you’ve already applied. On a branch with several unrelated WIP stashes from different days, running clear to “tidy up” can permanently erase work nobody has retrieved yet. Drop entries individually by index once you’ve confirmed each one is no longer needed, and reserve clear for when you’re certain the entire stack is disposable.
Best Practices
- Always attach a message with
git stash push -m "<message>"— the defaultWIP on <branch>label becomes useless once you have more than one or two entries. - Run
git stash listimmediately before anyapply,pop, ordrop, especially after dropping other entries earlier in the same session. - Prefer
git stash applyovergit stash popwhen you’re not fully confident about the target branch — it lets you verify the result and drop explicitly afterward. - Use
git stash show -p stash@{n}to preview a stash’s contents before applying it, the same way you’d review a diff before merging. - Reach for
git stash branch <name> stash@{n}whenever a direct apply is likely to conflict — it rebuilds the original branch context instead of forcing a merge on top of newer commits. - Include
-uwhenever new, untracked files are part of the work you’re setting aside — don’t assume plaingit stashcaptures everything. - Don’t let the stash stack become a substitute for commits. If work will live longer than a few minutes, commit it to a short-lived WIP branch instead — commits are safer, diffable, and don’t get reshuffled by index.
- Periodically run
git stash liston shared machines or long-running projects and drop anything stale — a growing, unlabeled stash stack is a common source of “where did my changes go?” confusion.
Practice Exercises
- Create three stashes in a row, each with a distinct
-mmessage, from three different branches. Rungit stash listand confirm you can identify which entry belongs to which branch purely from the messages, without applying anything. - Push a stash that includes both a modified tracked file and a brand-new untracked file, first without
-uand then with it. Comparegit statusafter each to confirm exactly which files were captured. - With at least two stashes on the stack, apply the older one (
stash@{1}) onto a fresh branch usinggit stash branch, and confirm viagit stash listthat only that specific entry was removed while the other remains.
Summary
- Git stores stashes as real commit objects referenced by
refs/stash, with the reflog of that single ref providing the numberedstash@{n}history. stash@{0}is always the most recently created entry; indices shift down automatically whenever an earlier entry is dropped or popped.git stash listandgit stash show [-p]let you identify and inspect entries safely before touching anything.applykeeps the stack entry around for safety;popremoves it only on a clean apply;dropremoves it without applying.git stash branchis the safest way to recover from a stash that would otherwise conflict with your current branch.- Always label stashes with
-m, re-checkgit stash listbefore acting on an index, and avoidgit stash clearunless you’re certain the entire stack is disposable.
