1. Introduction
The Git stash is a mechanism for storing changes without making commits, thereby enabling work on other branches without losing the current progress. While we can stash the complete bundle of changes, including staged and unstaged files, we can also pick and preserve just part of the modifications.
In this tutorial, we explore ways to stash only certain files and changes. First, we create a simple repository and make some modifications. After that, we demonstrate how to use the default stash action for selecting separate files directly on the command line. Next, we check another way to do the same. Then, we briefly refresh our knowledge about patching. Finally, we delve into the interactive patch option of the stash subcommand as a way to stash only some changes.
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. Sample Repository
To begin with, let’s make some modifications to an existing repository and check its status:
$ echo "$(date)" >> trackedfile
$ touch untrackedfile
$ rm deletedfile
$ 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")
Let’s assume we restore this state at the beginning of each section.
While there are many ways, several main methods exist in recent git versions that enable us to select a subset of these changes for stashing.
3. push With Arguments
Git has had the push operation of the stash subcommand as a way to create a stash for a long time:
$ git stash push
In fact, push is the default when just running git stash.
More recently, support was added for a list of direct arguments to push, which represent files to include in the pushed stash:
$ git stash [push] [--] <FILE_PATH_1> <FILE_PATH_2> ... <FILE_PATH_N>
Notably, both push and the — direct argument cutoff are optional. However, it’s usually good to have at least one of the two to avoid syntax confusion.
Let’s see an example by stashing only one of the current branch changes:
$ git stash -- deletedfile
Saved working directory and index state WIP on master: 9d7f6b2 last commit message
At this point, we have a new stash:
$ git stash list
stash@{0}: WIP on master: 9d7f6b2 last commit message
Further, we can explore its contents:
$ git stash show 0
deletedfile | 0
1 file changed, 0 insertions(+), 0 deletions(-)
So, only the removed deletedfile file is in the new stash.
This means all other changes remain in the working tree or staging:
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
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")
As we already saw in the general syntax, should we want to specify more than one file, we can do so via a list with space separators.
4. push With File Containing File Paths
In addition to the whitespace-separated list we can pass to push, Git also supports the –pathspec-from-file option:
$ git stash --pathspec-from-file=<LIST_FILE_PATH>
Basically, we pass a path to –pathspec-from-file that points to a file, containing a list of paths to the files we want to stash. Of course, these should be relative to the current working directory.
Let’s see an example with a file that contains the paths to two of the working tree files:
$ cat filelist
trackedfile
deletedfile
$ git stash --pathspec-from-file=filelist
Saved working directory and index state WIP on master: 9d7f6b2 last commit message
Now, we can verify the files are within this new stash:
$ git stash show
deletedfile | 0
trackedfile | 1 +
2 files changed, 1 insertion(+)
Without a stash number as an argument, show outputs the details of the latest stash.
To avoid problems with special filenames, we can also use the –pathspec-file-nul flag and separate the file paths in the list file with NULL characters.
5. push Untracked File
Even when specifying them explicitly, we can’t directly stash untracked objects:
$ git stash -- untrackedfile
error: pathspec ':(,prefix:0)untrackedfile' did not match any file(s) known to git
Did you forget to 'git add'?
To include untracked files in a stash, we always use the –include-untracked (-u) flag:
$ git stash --include-untracked -- untrackedfile
Saved working directory and index state WIP on master: 9d7f6b2 last commit message
Notably, this also holds when the list is within a file, i.e., when using the –pathspec-from-file flag.
6. Git Patching
Several Git commands support the –patch (-p) flag. In short, –patch separates changes into so-called diff hunks and provides an interactive prompt for deciding what to do with each one:
- y: stage
- n: do not stage
- q: do not stage this hunk or any of the remaining ones, i.e., quit
- a: stage this hunk and all following hunks in the same file
- d: do not stage this hunk or any following hunks in the same file
- s: split the current hunk into smaller hunks
- e: manually edit the current hunk
Usually, we can also input ? to get help.
Effectively, a hunk represents part of the diff changes between two commits.
For example, we can stage separate hunks with add, which understands the –patch flag:
$ git add --patch
diff --git a/deletedfile b/deletedfile
deleted file mode 100644
index e69de29..0000000
(1/1) Stage deletion [y,n,q,a,d,?]? y
diff --git a/trackedfile b/trackedfile
index e69de29..ee82e24 100644
--- a/trackedfile
+++ b/trackedfile
@@ -0,0 +1 @@
+Sun Feb 22 10:02:10 AM EST 2024
(1/1) Stage this hunk [y,n,q,a,d,e,?]? n
[...]
For each partial diff, we get an overview and a prompt. Importantly, even if we cancel the procedure with Ctrl+C, any selected actions are applied.
7. Stash Patching
When it comes to the stash subcommand, both the save and push operations support the –patch flag.
In the context of stashing, –patch enables the interactive selection of hunks generated by the differences between the current HEAD and the working tree. As a result, the stash index state is identical to that of the repository.
Notably, each stashed hunk is then rolled back from the working tree.
Let’s see an example:
$ git stash push --patch
diff --git a/deletedfile b/deletedfile
deleted file mode 100644
index e69de29..0000000
(1/1) Stash deletion [y,n,q,a,d,?]? y
diff --git a/trackedfile b/trackedfile
index e69de29..ee82e24 100644
--- a/trackedfile
+++ b/trackedfile
@@ -0,0 +1 @@
+Sun Feb 22 10:02:10 AM EST 2024
(1/1) Stash this hunk [y,n,q,a,d,e,?]? n
[...]
Same as before, push is optional, since it’s the default.
Unlike the add subcommand, this operation is atomic, meaning that aborting the procedure via Ctrl+C won’t apply any operations.
Notably, –patch implies –keep-index, which can stash staged changes, but preserves the staging tree intact. Thus, we might need to use –no-keep-index to avoid this.
Of course, untracked files aren’t considered unless explicitly requested via –include-untracked.
8. Summary
In this article, we explored different ways to stash selected files and changes in a Git repository.
In conclusion, stashing can be very helpful, especially for preserving only part of the modifications at a given time.