Everyday Git: Stashing

This post is part of Everyday Git, a series that explores Git features from the command line, with real-world usage in mind.

Introduction

Imagine you’re in the middle of developing a new feature on a separate branch. You’ve been experimenting with some changes, but you’re not quite ready to commit or push them yet. Suddenly, your boss walks in and asks you to drop everything and fix a critical bug — now.

You’re stuck. You have local changes you don’t want to lose or push, but you also don’t want to mix them with the quick bug fix. Switching branches isn’t possible while your working directory is dirty. So what now?

This is exactly where Git stash comes in.


The basics of git stash

At its core, git stash is a way to:

record the current state of the working directory and the index (while allowing) to go back to a clean working directory. - man git-stash(1)

This saves all local changes and reverts the working directory to match the HEAD commit.

In the following example, foo.txt has been modified, so the working directory is dirty. Running git stash saves the local modifications to a new stash entry and rolls the working directory back to HEAD. A following git status shows what we’d expect, a clean working directory.

-- 👇 At HEAD
$ git log
commit a069749 (HEAD -> main, origin/main, origin/HEAD)

    initial commit.

$ ls
foo.txt

-- 👇 Make a change to foo.txt
$ echo "foo" >> foo.txt

$ git status
Changes not staged for commit:
  modified: foo.txt

-- 👇 Stash changes
$ git stash
Saved working directory and index state WIP on main: a069749 initial commit.

-- 👇 Working tree is clean
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean

It’s worth noting that, like most Git commands, git stash can be passed the argument [-p | --patch] to interactively choose hunks to stash.

Viewing the stash

To view all stashed entries, git stash list can be used.

Each stash entry is listed with its name, the name of the branch that was current when the entry was made, and a short description of the commit the entry was based on. - man git-stash(1)

The stash works like a stack - the latest stashed changes will always be stash@{0}.

$ git stash list
stash@{0}: WIP on main: a069749 initial commit.

In addition to git stash list, there may be times when the actual changes within a stash need to be seen. git stash show [<stash>] can be used for this purpose.

git stash show displays

the changes recorded in the stash entry as a diff between the stashed contents and the commit back when the stash entry was first created. - man git-stash(1)

By default, git stash show will display the diffstat however, it accepts any formatting options known to git diff. This allows arguments such as -p to be used to see the changes in patch form.

$ git stash list
stash@{0}: WIP on main: a069749 initial commit.
stash@{1}: WIP on main: a069749 initial commit.

$ git stash show 0
foo.txt | 1 +
1 file changed, 1 insertion(+)

$ git stash show -p 0
diff --git a/foo.txt b/foo.txt
index a069749..855c0bf 100644
--- a/foo.txt
+++ b/foo.txt
@@ -1 +1,4 @@
+ foo

These defaults can be changed via the stash.showStat, stash.showPatch, and stash.showIncludeUntracked config variables when no <diff-option> is provided.

Looking back at the stashed changes, it was hard to figure out what each stash represented without further inspection. git stash is actually a shorthand for git stash push [(-m | --message) <message>]. git stash push allows an optional message to be passed giving more description to a stash entry.

$ git stash push -m "Add content"
Saved working directory and index state On main: Add content

$ git stash list
stash@{0}: On main: Add content

Retrieving stashed changes

We’ve stashed our changes, seen that they’ve been stashed, and even added a message to make it easier to find. We’ve fixed the reported bug and we’re ready to start working again on our feature. There are two main ways to recover the stashed changes, git stash pop [--index] [<stash>] and git stash apply [--index] [<stash>].

So how do these differ?

git stash pop [--index] [<stash>] will:

Remove a single stashed state from the stash list and apply it on top of the current working tree state. - man git-stash(1)

If no additional arguments are passed, git stash pop will remove the top entry from the stash and apply it back to the working tree. This can fail due to conflicts and in these cases, the entry will not be removed from the stash list. Conflicts need to be resolved by hand, with git stash drop needing to be called manually after to remove the entry from the stash.

The additional arguments allow stash entries to be picked via stash index, e.g. 0, 1, or stash entry name, e.g. stash@{n}.

-- 👇 Pops `stash@{0}` from stash
$ git stash pop
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
    modified: foo.txt

no changes added to commit
Dropped refs/stash@{0} (a069749)

-- 👇 Nothing in the stash
$ git stash list

git stash apply [--index] [<stash>] will:

Like pop, (apply the stashed changes to the working tree state), but (will) not remove the state from the stash list. - man git-stash(1)

There’s one more difference worth mentioning here. <stash> may be any commit that looks like a commit created by stash push or stash create. You don’t have to use a stash@{n} reference. You can use any commit SHA that was created by git stash push or git stash create. To understand what this means, let’s dive a little deeper into what a stash entry actually is.

When a stash is created, behind the scenes, each stash is just a special kind of commit (actually, 3 commits: one for the working tree, one for the index, and one for untracked files).

The ancestry graph looks like this:

       .----W
      /    /
-----H----I

where H is the HEAD commit, I is a commit that records the state of the index, and W is a commit that records the state of the working tree.

You can manually create a stash using git stash create. This creates a stash entry and returns its object name without storing it anywhere in the ref namespace. Do note that git stash create isn’t typically useful for everyday workflows - it’s mainly intended for scripting.

Merge conflicts

As mentioned before, applying a stash may result in merge conflicts. Sometimes, those merge conflicts are so large that it’s easier to create a new branch from the stashed changes and continue work from there.

git stash branch <branchname> [<stash>] allows for just that. It will create and checkout a new branch named <branchname> starting from the commit at which the <stash> was originally created. It then applies the stashed changes to the new working tree and index. If this succeeds, it then drops the <stash> entry.

Clearing the stash

git stash clear will remove all stash entries. Only run this if you’re sure you don’t need the stashed changes anymore as once removed, the entries will be subject to pruning and may be impossible to recover.

Note, as per the Git man pages, it may be possible to recover mistakenly dropped or cleared stash entries. The following “incantation” will attempt to recover a list of stash entries that are still in your repository but no longer reachable.

git fsck --unreachable | 
grep commit | cut -d\  -f3 | 
xargs git log --merges --no-walk --grep=WIP 

git stash drop [<stash>] is a little more safe and simply removes the specified entry from the stash.

Scripting

Git was designed not just as a user facing tool but also as a scripting tool. As such, there are some commands that are intended to be used for scripting purposes and not directly run by users. git stash create and git stash store are two examples of this.

As mentioned earlier, git stash create is a way to create a stash entry without storing it anywhere in the ref namespace. git stash store is a direct companion to this command and is a way to store a given stash in the stash ref in addition to updating the stash reflog.

Deprecated

There’s one additional git stash related command we haven’t talked about yet and that’s git stash save. This option is deprecated in favour of git stash push.

It behaves in a very similar way to git stash push with the main difference being that it cannot take a pathspec - the list of files to stash. Instead, all non-option arguments are concatenated to form the stash message.

Conclusion

As with any CLI tool, Git commands have myriad of additional options and edge cases. The goal of this series is to give you a strong foundation and help you confidently explore deeper when you’re ready.

Whether you’re dropping everything to fix a bug or just experimenting freely, stashing gives you room to breathe.

Thanks for reading,
- Brook ❤