Stashing Work in Progress
Put work down safely and pick it up again: push with a message, apply versus pop, the untracked-file trap, partial stashes, what a stash really is, and recovering a dropped one.
Put work down safely and pick it up again: push with a message, apply versus pop, the untracked-file trap, partial stashes, what a stash really is, and recovering a dropped one.
You are four files into a refactor that does not compile yet when the alert lands: production is down, and the one-line fix is yours to write. The work in front of you is nowhere near committable, and it has nothing to do with the fix.
Git will often let you switch branches with those changes still in
your working tree, and that is worse than being stopped. Your
half-finished refactor is now sitting on the hotfix branch, one
absent-minded git commit -a away from shipping. git stash is
the way out. By the end of this lesson you will know exactly what
it stores and where, which of its verbs to trust, how it loses new
files if you let it, and the one situation where you should reach
for a branch instead.
Stashing takes everything that differs from HEAD in your
tracked files — staged and unstaged alike — records it somewhere
safe, and resets your working tree and index back to a clean HEAD.
You get a pristine checkout without throwing anything away.
git stash push -m "wip: parser handles trailing commas"
git switch main # clean tree, so this is a plain switch
git switch -c hotfix/login
# ...fix, commit, push, breathe...
git switch parser-work
git stash applyPrefer git switch to git checkout for this. checkout aimed at
a path silently discards uncommitted changes to that path, so a
mistyped branch name can cost you work; switch only ever moves you
between branches and refuses when it cannot do so safely.
That is the shape of the whole feature: put it down, go somewhere else, pick it up again.
A bare git stash is shorthand for git stash push with a message
Git writes for you, and Git's message describes where you were
standing rather than what you were doing:
stash@{0}: WIP on parser-work: 4d7e0aa Fix changelog typoThat names the branch and the commit you happened to be sitting on.
It says nothing about the change. One stash is fine — you remember.
By the third interruption in a day you have three entries that read
almost identically, all pointing at the same base commit, and you
are running git stash show -p on each one to work out which is
which. It is a freezer full of unlabelled containers.
Three commands cover inspection, in increasing detail. list shows
the stack, show gives a diffstat, and show -p gives the full
patch.
The default list output omits dates, which is exactly the field
you want when deciding whether an entry is still worth keeping.
git stash list accepts git log formatting options, so you can
ask for age directly:
A stash you made ninety minutes ago is a paused task. A stash from three weeks ago is a decision you have been avoiding.
There are two ways to get work back, and the difference is one word wide.
Applies it, then deletes it.
Deleted the instant the apply succeeds — which is precisely the moment you have not yet checked whether it landed on the branch you meant, or whether the result still builds.
Applies it and leaves it on the stack.
You look first, confirm it went where you wanted, and then
git stash drop on purpose.
Make this the habit.
Without --index, everything arrives as unstaged changes and the
staging you had carefully built up is flattened. With --index,
Git restores what was staged as staged — and refuses the whole
operation if it cannot reproduce that split exactly, which is the
right failure.
Two more verbs finish the set. git stash drop removes one entry.
git stash clear removes every entry at once, with no confirmation
and no summary of what it deleted. Those changes were never on a
branch and nothing routine brings them back — see the last section
for the unpleasant way to recover them.
Here is the trap that reads as data loss. git stash saves changes
to files Git already tracks. A file you created and never git added is not a change to a tracked file — it is not part of the
snapshot at all, so it stays exactly where it is, in your working
tree, and follows you to whatever branch you switch to.
-u (--include-untracked) sweeps up new files as well.
-a (--all) additionally takes ignored files — build output,
node_modules, local env files — which is almost never what you
want and can be very slow in a large repository. Reach for -u by
default and -a only when you deliberately want a bare directory.
The interruption rarely touches everything you have open. If the hotfix lives in the same files as your refactor you need the whole tree clean, but often you only need part of it out of the way — the debug logging, the temporary CSS, the one file with the broken import.
The first form takes a pathspec and stashes only those paths. The
second drops you into the same interactive hunk picker as
git add -p, so you can stash individual hunks and keep the rest in
your tree. Note that -p implies --keep-index unless you pass
--no-keep-index.
That flag is useful on its own, for a different job: git stash push --keep-index stashes everything but leaves your staged changes in
the working tree, so you can run the test suite against exactly what
you are about to commit and nothing else.
A stash is not a special file format or a hidden directory. It is a
commit — in fact two or three commits — parked on a ref called
refs/stash that no branch points to.
The main stash entry is a merge commit. Its first parent is the
commit you were on, its second parent is a commit holding the index
as it was, and if you passed -u there is a third parent holding
your untracked files. The entry's own tree is your working tree at
the moment you stashed. That is the entire mechanism.
The stack is the reflog of refs/stash, which is why
stash@{1} looks like reflog syntax — it is reflog syntax,
meaning "one entry back in the history of this ref". Two consequences
follow directly, and they are the ones that confuse people.
First, stashes are ordinary commits, so they survive branch
switches, rebases, and reboots, and because refs/stash belongs to
the repository rather than to a branch, an entry made on one branch
is visible and applicable from every branch. Second, the numbers are
positions, not names: drop stash@{1} and what was stash@{2}
becomes stash@{1}. Never take a list, then act on the numbers you
read a few commands ago.
The internals also explain git stash branch. Because the entry
records its base commit as the first parent, Git can recreate the
world as it was:
That creates a branch at the commit the stash was made from, checks
it out, applies the stash there with --index, and drops the entry
if all of that succeeds. It is the answer to a stash that has aged
past its base — when the branch has moved on so far that a plain
apply is a wall of conflicts, and to the "I stashed this on the
wrong branch" mistake.
Now the opinion, because the tool is routinely used past its range.
Excellent for minutes. Poor as storage.
No branch name, no message beyond one line, no presence in
git log. It does not push, so it is not backed up and
nobody can take over from you.
And a single git stash clear — yours or a script's — takes
the lot without asking.
Pushable, nameable, and not embarrassing.
Backed up the moment you push it, visible in git branch,
and shareable when somebody has to pick it up.
Because you already know interactive rebase, the WIP commit is temporary — you squash or reword it before review.
For anything you will not come back to within the hour, commit instead:
Now the work exists twice, on two machines, under a name anyone can find.
Dropping or clearing a stash does not erase the underlying commit immediately. It only removes the reference, leaving the commit unreachable and waiting for garbage collection, which typically will not touch it for a couple of weeks.
The easy path: git stash drop prints the hash it just dropped.
Copy that line before you clear your terminal and recovery is a single command. If the hash is gone, hunt for it:
If git gc has already run and collected the commit, it is gone.
This is the strongest practical argument for the previous section:
a WIP commit on a pushed branch never gets you here.
You can now put work down mid-sentence and pick it up cleanly: name
every entry, inspect before restoring, prefer apply to pop,
remember -u when new files are involved, and promote anything
long-lived to a branch before the stack becomes a graveyard.
Next comes cherry-picking and moving commits between branches, which answers the neighbouring question: not "how do I put this down for ten minutes" but "this change is committed on the wrong branch, and the right one needs it now". The internals you just learned carry straight over, because a cherry-pick is another small merge wearing a different name.
The way to make this stick is to run the interruption for real.
Start a change that includes at least one brand-new file, stash it
without -u, switch branches, and watch the new file follow you.
Then do it again with -u and watch it disappear and come back.
That one experiment teaches more than any list of flags.
Dropped stash@{0} (b6f2c1e4a7d3f9b0c5e8a2d7f4b1c6e9a3d0f7b2)git stash list # the whole stack, newest first
git stash show stash@{1} # which files, how many lines
git stash show -p stash@{1} # the actual diff
git stash show -p -u stash@{1} # include untracked files, if anygit stash list --format='%gd %cr %gs' # id, age, messagegit stash apply stash@{1} # any entry, not only the newest
git stash apply --index # restore the staged/unstaged split too
git stash drop stash@{1} # once you are satisfiedgit stash push -u -m "wip: new adapter module" # + untracked files
git stash push -a -m "wip: everything" # + ignored filesgit stash push -m "css tweaks" -- src/styles/ app/theme.css
git stash push -p -m "debug logging, out of the way"git stash branch parser-commas stash@{0}git switch -c wip/parser-commas
git add -A
git commit -m "WIP: parser, trailing commas"
git push -u origin wip/parser-commas # now it exists twicegit fsck --unreachable | grep commit # dangling commits, newest last
git show <hash> # is this the one?
git stash apply <hash> # bring it back
git stash store -m "recovered" <hash> # or re-attach it to the stackgit stash push -m "msg" # stash tracked changes, named
git stash push -u -m "msg" # ...plus untracked files
git stash push -a -m "msg" # ...plus ignored files too
git stash push -m "msg" -- <path> # stash only these paths
git stash push -p -m "msg" # choose hunks interactively
git stash push --keep-index # leave staged changes in the tree
git stash list # the stack, newest first
git stash list --format='%gd %cr %gs' # ids, ages, messages
git stash show stash@{1} # diffstat for one entry
git stash show -p stash@{1} # full patch for one entry
git stash show -p -u stash@{1} # ...including untracked files
git stash apply stash@{1} # restore, keep the entry
git stash apply --index # also restore the staged split
git stash pop # restore and delete the entry
git stash drop stash@{1} # delete one entry (prints its hash)
git stash clear # delete every entry, silently
git stash branch <name> stash@{0} # rebuild the base, then apply
git fsck --unreachable | grep commit # hunt a dropped entry
git stash apply <hash> # recover it by hash, never cherry-pick
git stash store -m "msg" <hash> # put a loose entry back on the stack