TL;DR
You see four stashed files because Git really has "stashed" four files. This abuses the word "stash" rather badly, but the four stashed files are the three you had already staged-for-commit plus the one more than git stash push -- <pathspec>
staged for commit into the working-tree commit that git stash push
makes.
Long
The git stash
documentation is, in my opinion, somewhat lacking. It's particularly bad for your case. The documentation is better now than it was 15 years ago, but it's still not great. (Unfortunately, fixing it is hard.)
Fundamentally, git stash
is about making two, or sometimes three, commits. I like to refer to these as the i
, w
, and (optional) u
commits. The i
commit saves the index state, the w
commit saves the working tree state, and the u
commit, if it exists, saves some or all untracked files.
These three commits are tied together so that a single commit hash ID can locate all three of them (or the two, for the two-commit case). Diagrammatically, we can draw these three commits like this:
...--o--o--@ <-- current-branch (HEAD)
|\
i-w <-- refs/stash
/
u
That is, the refs/stash
ref (which is not a branch name) will contain the hash ID of the w
commit, when all is said and done. The w
commit has the form of a merge commit because it has two or three parents: the first parent of w
is @
, the current commit; the second parent of w
is i
, the index commit; and the third parent if it exists is u
, the untracked-files commit.
Now, the index, as is so clearly explained 🙄 in all Git documentation, always contains every tracked file: that's the actual definition of a tracked file, after all.1 So git stash
typically makes the i
commit's snapshot by doing a simple git write-tree
, which just writes out whatever is in the index right now, exactly the way git commit
would. Then Git does the equivalent of git add -u
to add all modified working-tree files and does another git write-tree
to make this snapshot. If there's no need for a u
commit—as in the default git stash save
or git stash push
case—we're now done making trees and can use git commit-tree
to write out the two commits as commit objects, and then update the refs/stash
ref with git update-ref
(making sure to have reflogs turned on in case there's already an existing stash).
Having made the two commits, we now just have to git reset --hard
to make the existing index and working tree match the current commit, and that's what the original git stash
code did, more or less.
Alas, that's not the end of things: if it were, git stash
wouldn't be so awful. But no, we now get a bunch of extra flavors of stashing:
git stash -u
, git stash -a
: these make that third commit;
--keep-index
: this changes how the git reset
works;
- new! "partial stash" with pathspecs! (Now how much would you pay?)
I'll skip describing the -u
and -a
options except to say that these result in stashes that are very hard to un-stash sometimes. The --keep-index
option basically means that instead of resetting Git to the current commit, we have Git reset to the saved index commit, which is intended to be (and on rare occasions, really actually is) useful for testing the stashed index. Unfortunately once the testing is done we must git reset --hard
before we can restore things, and then we must carefully remember to git stash apply --index
and if we forget, we've just destroyed our test version. To keep this from becoming a complete rant (it's already most of the way there), let me stop here with this part and direct folks to pre-commit.com, which is a much better plan than git stash --keep-index
. Let's move on to the heart of the original question, about git stash push -- <pathspec>...
.
1Note the oddity of git add -N
, which makes an index entry for a "new" file that's not quite tracked. Git behaves badly with such an index entry, to the point of refusing to make new stashes, so there's that. (Insert 😈 and/or 🤢🤮 faces here.)
Git-stash-push with pathspecs
Now, I know the question asked about git stash show
, but to get there, we have to address the push-with-pathspecs part. What this does is affect what goes into stash commits and what gets git reset
(or git clean
-ed) in a complex way. You're still going to get the same two or three commits: that's still fundamental to git stash
.
Remember, the index already holds every file. If you've run git add
on some files, it already holds different versions of those files than the versions that appear in the current (HEAD
or @
) commit. That's the case in your setup, as this:
$ git status
On branch some/0.0.1
Your branch is up to date with 'origin/some/0.0.1'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: src/main/java/ServiceServiceImpl.java
modified: src/main/java/util/ServiceUtil.java
modified: src/test/SystemTest.java
shows up before you run git stash push
. Note the three changes to be committed files, which are three files that are updated in Git's index.
You now run (I'll re-order the arguments for @matt 😀):
git stash push -m "app.xml stashed" -- src/main/resources/application.yml
The pathspec argument you give here effectively needs to run git add src/main/resources/application.yml
. But git stash push
with pathspecs wants to save, in its i
commit, the current index, so it does that just as before. This saved index thus has three files modified, as compared to the current commit.
Then it wants to save, in its w
commit, the current index as modified by git add
commands. So it does that: it adds this fourth file and uses git write-tree
(or internal equivalent). This new w
commit thus has all four files modified, as compared to the current commit.
If you are making a third commit at this point, git stash push
with pathspecs limits the contents of that third commit to the untracked files named by the pathspec. It then removes only those files from the working tree (using git clean
or equivalent).4
Then, having made this stash, Git will restore (as in the equivalent of git restore -W --source HEAD
) each working-tree file that matches the pathspec. Here, that's just the one file, so your working tree retains the modified, staged-for-commit files as modified, staged-for-commit files.
The tricky thing about all of this is that because three files were already staged for commit, and the index used to make the w
commit got one more git add
-ed, your stash really does have exactly those four files as updated files in the w
commit.
Note that if start with a clean slate:
git reset --hard
(suppose this gets us three files f1, f2, f3) then modify some files:
echo modified >> f1; echo modified >> f2
and git add
the first one:
git add f1
and then modify it again:
echo doubly modified >> f1
and run git stash push -- f2
, Git:
- saves the current index as the
i
commit, so i
's f1 has modified
but its f2 and f3 don't;
git add
s f2, and saves the result as the w
commit, so w
's f1 and f2 both have modified
added; and
git restore
s only file f2, so that f1 in the working tree retains its "doubly modified", but f2 in the working tree is now unmodified.
The git stash
command does all this fancy footwork using a temporary index (that starts out as a copy of the real index), rather than the real index, so the real index remains undisturbed. The staged-for-commit f1
(singly modified) is still staged-for-commit, as git diff --staged
will show.
This is horribly complicated. Personally, I recommend avoiding git stash
.
4Early on in its implementation, git stash push -u
or git stash push -a
with pathspecs had a rather dreadful bug, where it would stash the specified files in the u
commit, but then clean away far too many files. Since these were untracked files, they were irrecoverable!
src/main/resources/application.yml
appears to be such apathspec
--
is just a generic argument used to say "don't parse the rest of this line as options"; it's useful to avoid file names starting with-
being mistaken for options, but otherwise unnecessary.