1. Introduction

Due to its version-tracking nature, Git offers many features for handling changes. One option we have is the special stash area. This is a separate space for storing the latest staged or unstaged uncommitted changes in the working tree.

In this tutorial, we look at the Git stage and stash areas. First, we explore staging. After that, we explain situations, in which the Git stage mechanism may not be enough. Finally, we go through the Git stash, from basics to more complex use cases.

We tested the code in this tutorial on Debian 12 (Bookworm) with GNU Bash 5.2.15. Unless otherwise specified, it should work in most POSIX-compliant environments.

2. Git Stage

Of the three Git trees, we mainly make changes to the working tree, i.e., the working directory of the repository:

$ echo more >> file1
$ cat <<EOI >file2
> new
> EOI
$ rm file3

Once we do, the status subcommand summarizes these modifications:

$ git status
On branch master
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   file1
        deleted:    file3

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        file2

no changes added to commit (use "git add" and/or "git commit -a")

Here, we can see the not staged (unstaged) changes. For each file, we can decide whether to include, i.e., stage, its new content in the current bundle of changes. In other words, we add it to the so-called staging tree:

$ git add file1
$ git add file2

In this case, we don’t stage the deletion of file3, but do stage the new versions of file1 and file2. Of course, we can still add or restore (unstage) files at this point.

Once we have a compilation of all desired changes in the staging area, we can again check the current status:

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   file1
        new file:   file2

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        deleted:    file3

Now, we see the staged changes under Changes to be committed.

At this point, a regular commit would submit this new content by attaching a node to the root tree and pointing to the relevant blobs.

3. Git Limbo

Sometimes, a situation might arise, in which we haven’t completed the current bundle of changes for the commit we plan to perform, but would like to check out another branch temporarily or permanently.

Let’s see what happens when we attempt to check out a branch with a modified working directory or non-empty stage:

$ git checkout branch1
error: Your local changes to the following files would be overwritten by checkout:
        file1
Please commit your changes or stash them before you switch branches.
Aborting

Although Git doesn’t need to touch the deleted file3 or the new file2, file1 would be modified relative to its current (staged or unstaged) version. Thus, the command doesn’t succeed, as that would mean we lose our changes.

The reason behind this is that Git doesn’t preserve the staging and working trees within the context of a branch. In fact, since a branch is only a pointer to a commit, all changes are normally lost upon checkout.

Yet, we can avoid this via a special feature.

4. Git Stash

The stash subcommand is a way for Git to preserve a snapshot of the current changes without staging or committing them. Of course, we can then use the same subcommand to restore the changes at a later point.

Let’s start with an unchanged working directory:

$ git status
On branch master
nothing to commit, working tree clean

Currently, the main master branch is checked out and the working tree is clean.

4.1. Perform Modifications

To begin with, we create an untracked file, modify an existing file, and delete one as well:

$ touch untrackedfile
$ echo $(date) >> trackedfile
$ rm deletedfile

At this point, git status has a record of all changes:

$ git status
On branch master
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        deleted:    deletedfile
        modified:   trackedfile

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        untrackedfile

no changes added to commit (use "git add" and/or "git commit -a")

Now, let’s assume we want to put our current work on hold and move on to another branch for a higher-priority task. Another reason might be that we aren’t really certain that the current modifications are adequate.

4.2. Create Stash

To preserve the whole working tree, we run the basic stash subcommand, which implies push, i.e., create a stash:

$ git stash
Saved working directory and index state WIP on master: 3fa666d last commit message

The WIP state here stands for Work In Progress, reflecting that this is unfinished work. The default message is WIP on : <LAST_COMMIT_ID> <LAST_COMMIT_MSG>, when we don’t specify one directly or via the usual –message (-m) option. If we do, it’s appended to On :

Notably, the default push function can receive a non-mandatory list of files to include directly or as a –pathspec-from-file, where is standard input.

Alternatively, the save subcommand of stash is similar but accepts a stash message as an argument and can’t cherrypick files.

Both save and the default push can process only the –staged (-S) files with the relevant flag.

Let’s verify the current status:

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        untrackedfile

nothing added to commit but untracked files present (use "git add" to track)

At this point, the only change remaining is untrackedfile, which we created, but didn’t add to the repository.

4.3. Untracked and Stash

It can be critical to understand that, by default, untracked files remain in the working directory tree even after a stash operation.

Still, we can ensure stashing includes all files via the –include-untracked (-u) flag:

$ git stash --include-untracked
Saved working directory and index state WIP on master: 3fa666d last commit message
$ git status
On branch master
nothing to commit, working tree clean

Another way to achieve the same is via the –all (-a) flag, which also includes ignored files.

In any case, now, even untrackedfile is stashed.

4.4. Inspect Stash Stack

The Git stash structure is a stack, containing each bundle created by a stash operation.

Let’s inspect the stash stack via the list subcommand of stash:

$ git stash list
stash@{0}: WIP on master: 3fa666d last commit message
stash@{1}: WIP on master: 3fa666d last commit message

In this case, we see the result of the two stash operations, one for the tracked and one for the untracked files. Each stash@{} element has a number within {} curly braces that identifies it, as well as the stash message.

Moreover, we can use Git log to see the stashes, as they appear like a detached commit tree:

$ git log --all --decorate --oneline --graph
*-.   46b69d2 (refs/stash) WIP on master: 3fa666d last commit message
|\ \
| | * 6def6bf untracked files on master: 3fa666d last commit message
| * 53095b4 index on master: 3fa666d last commit message
|/
[...]

Thus, we have a general overview.

4.5. show Stash

Next, we show details for a specific stash via its number:

$ git stash show 1
$ git stash show 0
 deletedfile | 0
 trackedfile | 1 +
 2 files changed, 1 insertion(+)

Importantly, due to the missing –include-untracked flag, we don’t see anything for stash 1:

$ git stash show --include-untracked 1
 untrackedfile | 0
 1 file changed, 0 insertions(+), 0 deletions(-)

The show subcommand further accepts diff arguments to augment its output.

4.6. Restore and Drop Stash

There are two ways to restore and delete a stash:

  • apply: apply the stashed changes to the working directory tree at HEAD
  • pop: works like apply, but also drop the stash from the stack
  • drop: remove a stash

To specify the stash we want to restore, we use its number for pop and any valid commit identifier for apply.

The working directory must match the index. Any conflicts prevent pop from dropping the element.

The drop itself can be done separately as well:

$ git stash drop <STASH_NUMBER>

The STASH_NUMBER can change with each removed stash.

In addition, clear drops all stash entries, so we should use it with caution.

4.7. Stash Options

In addition to the common stash operations, there are other ways to handle stashes.

In scripts, we can use create to generate stashes without storing a reference to them in the stash stack:

$ git stash create
087d502f375249eb1306660cea5822aa5e79d78d

Later on, if we do need to attach the reference to the refs/stash tree, store can create it from the output of create.

Separately, the branch subcommand of stash can create and check out a branch from the original stash commit, restoring the changes.

5. Summary

In this article, we explored the Git staging and stashing mechanics.

In conclusion, while staging is generally the standard way of handling changes, stashes are sometimes required when performing quick transitions between code paths.