2

Ran git stash to stash just one file out of 4 tracked files.

Using git stash show displays not only that file but the other staged files as well. Why? For example: why does it show anything other than the file "application.xml" that I stashed?

$ 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

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:   src/main/java/util/ServiceUtil.java
    modified:   src/main/resources/application-local.yml
    modified:   src/main/resources/application.yml
    modified:   src/main/resources/logback.xml


$ git stash push src/main/resources/application.yml -m "app.xml stashed" <-- stash just one file
Saved working directory and index state On 0.0.1: app.xml stashed

$ git stash list
stash@{0}: On 0.0.1: app.xml stashed

$ git stash show stash@{0} <---------------- Why does it show "staged" files other than the one I stashed (which is src/main/resources/application.yml)
 .../java/ServiceServiceImpl.java   |  8 ++++----
 .../java/util/ServiceUtil.java    |  1 +
 src/main/resources/application.yml            | 20 ++++++++++++++------
 .../test/SystemTest.java |  6 +++---
 4 files changed, 22 insertions(+), 13 deletions(-)

This is from the man git-stash:

show [-u|--include-untracked|--only-untracked] [] [] Show 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.

3
  • @matt That would have been my guess too, but the manual appears to say otherwise: if you provide a pathspec, then: "The new stash entry records the modified states only for the files that match the pathspec." In this example, src/main/resources/application.yml appears to be such a pathspec
    – IMSoP
    Commented Aug 25, 2022 at 18:26
  • @matt no it does not change the result
    – Khanna111
    Commented Aug 25, 2022 at 20:45
  • @matt There's nothing "incorrect" about swapping around parameter orders, and -- 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.
    – IMSoP
    Commented Aug 25, 2022 at 23:31

1 Answer 1

3

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 adds f2, and saves the result as the w commit, so w's f1 and f2 both have modified added; and
  • git restores 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!

4
  • Thanks for the very detailed answer. Where are the commits made to? To the local branch in repo? I'm asking about: "git stash is about making two, or sometimes three, commits"
    – Khanna111
    Commented Aug 26, 2022 at 17:02
  • The stash commits are on no branch, found only via refs/stash or its reflog. Branch names are refs that start with refs/heads/ and since refs/stash doesn't have heads/ after the refs/ part, it's not a branch name. So these commits are on no branches.
    – torek
    Commented Aug 26, 2022 at 22:45
  • Thanks. You avoid using stash. What is the replacement for that that you use?
    – Khanna111
    Commented Aug 29, 2022 at 16:48
  • I just make ordinary commits, on branches.
    – torek
    Commented Aug 29, 2022 at 23:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.