Switch context without losing changes
Anyone who suddenly needs to switch branches mid task knows the dilemma between half finished code and a clean working tree. git stash safely sets aside uncommitted changes, manages several named stashes at once, allows partial saving of individual hunks, and clearly distinguishes between pop and apply so interrupted work never gets lost.
Table of Contents
- 1. The Stash Mechanism: Commits on refs/stash Instead of a Clipboard
- 2. Fast Context Switching with git stash push
- 3. Named Stashes and Managing the Stash List
- 4. Partial Stashing with git stash push -p
- 5. git stash pop vs. git stash apply: The Difference That Matters
- 6. Capturing Untracked and Ignored Files When Stashing
- 7. Understanding and Resolving Stash Conflicts on Pop
- 8. Stashes and Branches: Loosely Tied, Never Restricted
- 9. git stash Compared to Improvised Workarounds
- 10. Summary
- 11. FAQ
1. The Stash Mechanism: Commits on refs/stash Instead of a Clipboard
git stash looks like a clipboard from the outside, but internally it is nothing more than a series of ordinary commits. On the first call, Git creates the refs/stash reference and writes a commit underneath it that has up to three parents: the commit HEAD pointed to at the time of stashing, a commit that captures the state of the staging area (the index), and another that captures the state of the working tree. If -u or --include-untracked is also used, a fourth parent with the untracked files at that point is added.
Every further git stash push does not create a new branch, but another commit whose predecessor is chained through the reflog of refs/stash, exactly like a normal branch reflog. That is why familiar commands such as git log, git show, or git diff work directly on a stash entry, for example git show stash@{0} or git diff stash@{0}^1 stash@{0}^2 to compare index changes against working tree changes. This commit-based nature also explains why a stash theoretically remains recoverable through git fsck even after it has been removed from the list, as long as garbage collection has not swept it up yet.
# Create the first stash and inspect its internal structure
git stash push -m "WIP: checkout refactor"
# refs/stash points at the newest stash commit
git rev-parse refs/stash
# a1c92f4e8b3d0176f2e4a9c8b1d3e5f7a9c1e3d5
# The stash commit itself, just like any other commit
git log --oneline -1 refs/stash
# a1c92f4 WIP on feature/checkout: 7f3e9a1 Refactor checkout service
git show --stat stash@{0}
# stash@{0}
# Merge: 7f3e9a1 9b2c410
# Author: M. Berger <m.berger@example.com>
# src/checkout/CartService.php | 12 ++++++------
# src/checkout/CartService.spec.js | 4 ++++
# Inspect the parents individually: base, index, working tree
git rev-list --parents -n 1 stash@{0}
# a1c92f4e 7f3e9a1d 9b2c4102
2. Fast Context Switching with git stash push
The most common trigger for git stash is an urgent context switch mid task: a hotfix needs to go on another branch right away, but the current state is neither finished nor commit worthy. git stash push (or simply git stash, which internally calls push with no options) moves all changes to tracked files in the index and working tree into a new stash entry and resets the working tree exactly to the state of HEAD, without leaving a commit behind.
The branch switch itself becomes trivial because git checkout or git switch can no longer report conflicts with uncommitted changes, since the working tree is clean. Once the interruption is dealt with, switching back to the original branch and running git stash pop is enough to restore the exact previous state of the index and working tree. This cycle of push, switch, handle the interruption, switch back, pop can be repeated as often as needed and is the core workflow stash was designed for in the first place.
# Mid work, a hotfix needs to go on main right now
git status
# modified: src/checkout/CartService.php
# modified: src/checkout/CartService.spec.js
git stash push -m "WIP: checkout refactor before hotfix"
# Saved working directory and index state On feature/checkout: WIP: checkout refactor before hotfix
git switch main
git switch -c hotfix/cart-discount-rounding
# ... write, test, commit, and merge the hotfix ...
git switch feature/checkout
git stash pop
# On branch feature/checkout
# Changes not staged for commit:
# modified: src/checkout/CartService.php
# modified: src/checkout/CartService.spec.js
# Dropped refs/stash@{0} (a1c92f4e8b3d0176f2e4a9c8b1d3e5f7a9c1e3d5)
3. Named Stashes and Managing the Stash List
Once more than one piece of work is interrupted at the same time, a single unnamed stash is no longer enough to keep track. git stash push -m "description" tags the new entry with a meaningful message that later shows up in git stash list next to the index placeholder stash@{0}, stash@{1}, and so on. Without a custom message, Git automatically generates a description from the branch name and the last commit, for example "WIP on feature/checkout: 7f3e9a1 Refactor checkout service", which becomes confusing quickly once several stashes pile up on the same branch.
The list behaves like a stack: the most recently created stash always gets index 0, and older entries automatically shift down. That means git stash pop with no argument always applies the most recent entry, while specifically addressed entries can be handled independently of their position through git stash pop stash@{2} or git stash apply stash@{1}. git stash show stash@{n} gives a compact diffstat, git stash show -p stash@{n} the full patch, both without actually applying the stash or removing it from the list.
# Save several parallel pieces of work as named stashes
git stash push -m "WIP: checkout discount rounding fix"
git stash push -m "WIP: experiment with lazy-loaded cart totals"
git stash push -m "WIP: temporary debug logging, do not ship"
git stash list
# stash@{0}: On feature/cart-totals: WIP: temporary debug logging, do not ship
# stash@{1}: On feature/cart-totals: WIP: experiment with lazy-loaded cart totals
# stash@{2}: On feature/checkout: WIP: checkout discount rounding fix
# Check a specific stash's diffstat without applying it
git stash show stash@{2}
# src/checkout/CartService.php | 12 ++++++------
# 1 file changed, 6 insertions(+), 6 deletions(-)
# View the full diff of a stash
git stash show -p stash@{1}
4. Partial Stashing with git stash push -p
Sometimes a change touches several logically independent things in the same working tree, for example a real bug fix and temporary debug code that should never be merged. git stash push -p starts an interactive hunk-by-hunk dialog, analogous to git add -p: for every contiguous block of changes, Git asks whether it should be included in the stash, with the familiar options y (yes), n (no), s (split the hunk further), e (edit manually), and q (abort).
The result is a stash that contains only the selected hunks, while the rest of the changes remain untouched in the working tree and can be committed right away. This technique is particularly valuable when a fix is discovered in the middle of a larger, still unfinished refactor: the fix gets separated, stashed immediately, applied on a clean branch, and committed, while the actual refactor stays interrupted in the working tree, without ever creating an intermediate commit for half finished code.
# Stash only selected hunks, the rest stays in the working tree
git stash push -p -m "WIP: only the rounding fix"
# diff --git a/src/checkout/CartService.php b/src/checkout/CartService.php
# @@ -42,7 +42,7 @@ class CartService
# - return round($total, 2);
# + return round($total, 2, PHP_ROUND_HALF_EVEN);
#
# Stash this hunk [y,n,q,a,d,s,e,?]? y
#
# @@ -88,3 +88,6 @@ class CartService
# + // TODO: remove before merging, only for local debugging
# + error_log('cart total: ' . $total);
#
# Stash this hunk [y,n,q,a,d,s,e,?]? n
git status
# modified: src/checkout/CartService.php
# (only the debug-log hunk is still visible, the rounding fix is stashed)
5. git stash pop vs. git stash apply: The Difference That Matters
git stash pop and git stash apply appear to do the same thing at first glance: they apply the changes saved in the stash to the current working tree. The difference lies in what happens afterward. pop is essentially an apply followed by an automatic drop, so the entry disappears from the stash list once it has been applied successfully. apply, on the other hand, leaves the entry untouched in the list, even after a successful application, and it has to be explicitly removed with git stash drop stash@{n} if needed.
This difference matters exactly when conflicts are a risk or when the same stash is needed more than once. During a risky pop that fails with a merge conflict, the stash entry stays in the list for safety, Git does not automatically drop it in that case, but that behavior should never be relied upon: whoever is unsure should test with apply first, review the result calmly, and only remove the entry afterward with drop. Anyone who wants to try the same stash on several branches, for instance to compare where it applies more cleanly, should also reach for apply instead of pop.
6. Capturing Untracked and Ignored Files When Stashing
A common pitfall: git stash push by default only captures changes to files Git already tracks, meaning everything in the index or working tree that belongs to a known file. Brand new files that were never added remain untouched in the working tree, even if they are inherently inseparable from the change being stashed, for example a newly created class already referenced by a modified existing file. After stashing and switching branches, this leads to a build that seems to fail for no reason, because the referenced new file is missing.
The -u flag, or --include-untracked, explicitly includes new, untracked files in the stash. Files covered by .gitignore, such as generated build artifacts or local configuration files, are still excluded from even that and additionally need -a, or --all, which is less often useful in practice since ignored files are usually deliberately kept outside version control. As a rule of thumb: whenever a fix includes new files, -u is not optional, it is a prerequisite for a complete, lossless stash.
# New, untracked files are NOT part of the stash by default
git status
# modified: src/checkout/CartService.php
# Untracked files:
# src/checkout/RoundingStrategy.php
git stash push -u -m "WIP: rounding strategy incl. new file"
# Saved working directory and index state On feature/checkout: WIP: rounding strategy incl. new file
# (RoundingStrategy.php is now part of the stash)
# Also include files covered by .gitignore, e.g. generated configs
git stash push -a -m "WIP: incl. ignored build artifacts"
# Apply a specific stash on a DIFFERENT branch
git switch release/2.6
git stash apply stash@{2}
# stash@{2} remains in the list, apply does not drop it
git stash list
# stash@{0}: On feature/checkout: WIP: incl. ignored build artifacts
# stash@{1}: On feature/checkout: WIP: rounding strategy incl. new file
# stash@{2}: On feature/checkout: WIP: checkout discount rounding fix
7. Understanding and Resolving Stash Conflicts on Pop
A stash always exists relative to a specific base commit. If the current branch has moved on since the stash was created, for example through new commits on the same branch or an intervening merge, git stash pop can end up touching the same lines as the stash patch and produce a real merge conflict, complete with the familiar conflict markers <<<<<<<, ======= and >>>>>>> right in the working tree. Importantly, Git only removes the stash entry from the list as a safety measure if the application succeeds completely without conflict, on a conflicting abort it stays in place.
After a conflict there are three sensible paths: resolve the conflict markers manually, add the files to the index with git add, and then deliberately remove the stash entry afterward with git stash drop, since it is not dropped automatically after a failed pop. Alternatively, the whole operation can be undone with git reset --merge to start over calmly. Anyone who wants to avoid conflicts altogether should first apply the stash as a trial on a fresh, clean branch with apply before actually popping it on the real target branch.
8. Stashes and Branches: Loosely Tied, Never Restricted
An often overlooked detail: a stash entry is not permanently tied to any branch. It is stored globally under refs/stash in the repository, regardless of which branch it was created on. The message "On feature/checkout: ..." is purely informational about its origin, not a technical restriction. That means a stash created on feature/checkout can just as easily be applied on main, on a release branch, or on a completely unrelated feature branch, as long as its content makes sense there.
The further the target branch and the stash's original base commit have diverged, however, the more likely conflicts become when applying it. For exactly this case, Git offers git stash branch <new-branch-name> [<stash>]: the command creates a new branch exactly at the stash's base commit, checks it out, and applies the stash where it is guaranteed to fit without conflict, since the branch branches off from precisely that starting point. On success the stash entry is automatically removed from the list, which makes this command the most robust way to still cleanly rescue an old, heavily diverged stash.
9. git stash Compared to Improvised Workarounds
Many of the problems around lost, incomplete, or hard to trace stashes are not caused by git stash itself, but by improvised or incomplete use of the command. Anyone who consistently creates unnamed stashes, forgets untracked files, or stashes an entire working tree when only part of it is relevant, produces exactly the confusion that stash is supposed to prevent. The table below contrasts five typical situations: the risky, incomplete approach on one side and the robust, recommended stash approach on the other.
| Situation | Risky approach | Recommended stash approach |
|---|---|---|
| New files belong to the fix | Plain git stash with no flags, loses untracked files | git stash push -u |
| Need the stash again later | git stash pop, removes it from the list, conflict risk | git stash apply for cross-branch reuse |
| Only part of it is relevant | Stashing the entire working tree at once | git stash push -p for partial stashing |
| Several parallel stashes | Unnamed stash, hard to identify later | git stash push -m "description" |
| "Playing it safe" with changes | Manually copying files aside into a temp directory | git stash as a safe, atomic operation |
The same pattern shows up in every row: the risky approach relies on luck or manual discipline, while the recommended stash approach uses Git's built-in safeguards, named, addressable entries, complete capture including new files, targeted partial selection, and a clear separation between one-time application and long-term reuse.
Mironsoft
Magento and Hyvä development with clean Git workflows
Want to stop losing changes on your Magento project?
We set up clear Git conventions for your Magento and Hyvä team so context switches, parallel stashes, and cross-branch work stay safe and traceable.
Git workflow audit
Branching, merge, and stash-friendly team conventions
Team onboarding
Hands-on Git training for development teams
Incident support
Fast help with lost or tangled stashes
10. Summary
git stash solves a problem every active repository runs into constantly: safely setting aside interrupted, not yet commit worthy work without losing it or creating a messy intermediate commit. Technically, a stash is nothing exotic, just a series of ordinary commits under refs/stash with up to four parents for the base, the index, the working tree, and optionally untracked files. The core workflow of git stash push, switching branches, handling the interruption, switching back, and git stash pop covers the vast majority of use cases, while named stashes with -m and git stash list keep several parallel pieces of work organized.
Three details deserve special attention: first, git stash push by default does not capture new, untracked files, which is exactly why -u is essential. Second, the difference between pop, which removes the entry, and apply, which keeps it for reuse, is decisive under conflict risk or cross-branch usage. And third, stashes are not tied to their branch of origin, git stash branch provides the cleanest way to still rescue an old stash without conflict when the target branch has diverged significantly.
git stash - The Essentials at a Glance
Commits, not a clipboard
Stash is implemented on refs/stash, with up to four parent commits for base, index, working tree, and untracked files.
Context switching
git stash push saves changes, switch branches, then git stash pop restores the state.
pop vs. apply
pop removes the entry and carries conflict risk, apply keeps it around for reuse.
Common pitfalls
-u for untracked files, -a for ignored files, git stash branch for heavily diverged branches.