Stashing content in git is very useful. when stashing untrack files and going through your stash like this
echo test > foo
git stash -u # foo is stashed
git stash show -p stash@{0}
untracked files are not shown. how can we see them?
thanks
You just need to look at the third stash commit. But "just need to" understates things a little bit, and this makes no sense until you know what the three stash commits are. To see what I mean, read on.
When you run git stash save
or git stash push
, Git's normal action is to create two commits, neither of which are on any branch. If you draw the "before" picture like this, you would have this series of commits:
...--o--o--* <-- branch (HEAD)
After git stash save
completes, you have two new commits that are not on branch branch
and not on any other branch either:
...--o--o--* <-- branch (HEAD)
|\
i-w <-- the stash
The w
commit saves the work-tree state while the i
commit saves the index. Each of these two commits is very much like any other commit, and in fact, the i
commit is made using most of the normal git commit
mechanism: Git writes the current index to an internal tree object using git write-tree
, then makes a commit object using git commit-tree
. If you don't know what these internal objects are, don't worry too much about that: the point is that this is also how git commit
does most of its job, by writing a tree and then a commit object. (The rest of git commit
consists of collecting your log message first, and updating the branch name at the end.)
The w
commit is a little more complicated because it has the form (but not intent) of a merge commit, with two parents instead of one. But basically it saves a snapshot of the work-tree, as if you had run git add
on all your tracked files.
When you add --all
or --include-untracked
(-a
or -u
for short), what git stash
does is that it adds a third commit, which we can draw this way:
...--o--o--* <-- branch (HEAD)
|\
i-w <-- the stash
/
u
The third commit is a snapshot, but it is a very odd snapshot. It contains only the untracked files—either the untracked-but-not-ignored files (git stash save -u
), or the untracked files including the untracked-and-ignored files (git stash save -a
). It also has no parent commit.
So we have 3 commits in total for:i
- indexw
- working-treeu
- untracked
One reason1 that Git added the push
verb, as in git stash push
—which is otherwise basically a synonym for save
—is that when you make a new stash, Git uses the stash reflog to keep track of earlier stashes. The current stash is stash@{0}
in reflog terms, and the earlier stash becomes stash@{1}
.
Each of these names is just a specific case of a more general thing that Git gives you: you can refer to any commit by any name that resolves to the correct hash ID. The "true name" of any commit is its big ugly hash ID. The gitrevisions documentation has the complete description of all the ways you can spell a hash ID to Git; using a name like branch
or stash
is one such way.
Using the name stash
locates commit w
specifically. Git then uses w
itself to find commit i
, and, if it exists, commit u
. Git can do this because each commit includes the hash IDs of its parent commits. What makes w
have the form of a merge commit is that it has at least two parents: i
, the index commit, and *
, the commit you were sitting on when you ran git stash save
or git stash push
in the first place.
We can modify most revision specifiers (like stash
) by adding a caret ^
character and a number to look specifically at the numbered parent. Writing stash^1
is a way of naming commit *
; writing stash^2
is a way of naming commit i
. If commit u
exists, writing stash^3
names it. Note that on some systems (Windows), ^
may be a special character that requires doubling or quoting, so instead of stash^3
you might need stash^^3
.
1The other reason was to add the ability to do partial stashing using pathspecs: git stash save
took any extra arguments as a message to include in the stash commits, so they needed a new verb that used -m
to specify the message, leaving room for pathspec arguments.
u
commitWe can view any commit using git show
. For stash w
commits this misfires because Git thinks that the w
commit is a merge, so we can use git stash show
instead. (It is a merge, just not one that git show
can show properly.) My earlier answer to a related question calls for using git diff
on the u
commit, because in that particular case, we don't want to get the header that git show
shows, but if we just want to look at the untracked file commit, it's OK to use git show
here:
git show stash^3
for instance. Here is the output for the foo
example above:
$ git show stash^3
commit 4c9bd2486706980f5a492d19c49270381db2d796
Author: Chris Torek <chris.torek gmail.com>
Date: Sun Sep 16 12:35:03 2018 -0700
untracked files on master: f72737e initial
diff --git a/foo b/foo
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/foo
@@ -0,0 +1 @@
+foo
Using a name like stash@{1}
identifies the commit in reflog entry #1, which is the next stash in your "stash stack". (The reflog starts counting from zero, so stash
and stash@{0}
mean the same commit.) So for stash@{1}
you would need stash@{1}^3
or perhaps stash@{1}^^3
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With