4

I'm having problems of doing

git stash pop

because of some dos2unix problems. I want to return to working on the stash without having to merge entire files which only differ only by line endings. I was thinking of returning to the stash after doing

git checkout BRANCHNAME

but I don't know how to determine the BRANCHNAME which represents the origin of this stash. Anybody any clue?

2 Answers2

7

TL;DR

Use git stash branch to create a new branch and apply the stash:

git stash branch newbranch

This always works (well, as long as your current status is all clean: see the long section below) and is the equivalent of creating a new branch whose parent is the commit that was current when you made the stash, then running git apply --index (meaning it restores the index and work-tree separately).

A literal answer to the question

All you need to do is find out whether any branch name(s) identify the same commit as the parent of the stash in question, so:

git for-each-ref --format='%(refname:short)' \
    --points-at $(git rev-parse refs/stash~1) refs/heads

will find the possible branch name(s), if there are any. (This relies on relatively modern features of git for-each-ref.)

Long

Each stash is itself just two, or sometimes three, commits. What git stash does is that it makes these two (or three) commits, but puts them on no branch.

The word branch in Git is itself a bit ambiguous (see What exactly do we mean by "branch"?), but here, we're using it in the sense that a branch name like master or develop simply points to the last commit on its branch:

...--E--F--G   <-- master
         \
          H   <-- develop

When you git checkout a branch name like develop, you are telling Git to:

  1. Extract the content of that commit into the index (the place where you build the next commit, also called the staging area or sometimes the cache)
  2. Take the now-unfrozen, but still Git-form / compressed files out of the index and copy them into the work-tree where you can work on them.
  3. Attach the name HEAD to the branch name, so that Git knows which commit you have checked out now:

    ...--E--F--G   <-- master
             \
              H   <-- develop (HEAD)
    

    Now Git knows that the commit whose hash is H is the current or HEAD commit, as pointed-to by the name develop.

If you make an ordinary new commit at this point (by copying updated or new files from the work-tree back into the index / staging-area, then running git commit to freeze them into a new commit), Git writes the new commit with the current commit as its parent, and then updates the branch name:

...--E--F--G   <-- master
         \
          H--I   <-- develop (HEAD)

The name HEAD remains attached to the branch name, but now the branch name identifies commit I instead of commit H.

What git stash does is to make two commits—one for the index, and one for the work-tree—that don't have a branch name. Instead, Git uses the name refs/stash to find the w commit (and w finds both the original commit and the i commit):

...--E--F--G   <-- master
         \
          H   <-- develop (HEAD)
          |\
          i-w   <-- refs/stash

After saving the i and w commits, git stash runs git reset --hard, to set the index and work-tree back to matching the current commit (here, H).

In this case, at this point in time, the parent commit of the stash is also pointed-to by the existing branch name develop. But suppose you have done this git stash, and then gone on to make a different set of changes and made a new commit I? Then you have:

...--E--F--G   <-- master
         \
          H--I   <-- develop (HEAD)
          |\
          i-w   <-- refs/stash

There is now no branch name pointing to existing, historical commit H, even though that's the one commit where refs/stash is guaranteed to apply.

So, if your index and work-tree are now clean (i.e., match the contents of commit I, to which HEAD is attached), and you now run git stash branch recover, what git stash will do is attach a new branch name, recover, to commit H, and check out that commit and make the new branch the current branch by attaching HEAD to it:

...--E--F--G   <-- master
         \
          H   <-- recover (HEAD)
           \
            I   <-- develop

and, as the last step of the git stash branch operation, apply and drop the stash, so that the index is back to the way the index was when you had H checked out and ran git stash, and the work-tree is back to the way the work-tree was when you had H checked out and ran git stash. You can now finish git adding and git commit to make new commit J:

...--E--F--G   <-- master
         \
          H--J   <-- recover (HEAD)
           \
            I   <-- develop

What if develop still points to commit H?

If you haven't moved the previous branch, well, now you have a new branch anyway. That is, now instead of the above drawing, you would have:

...--E--F--G   <-- master
         \
          H   <-- develop
           \
            J   <-- recover (HEAD)

(I left the new commit as J here to make it easier to match it up with the earlier diagram). There's nothing wrong with this setup. Of course if you don't want the recover branch here, you don't have to use git stash branch, but it does still work.

What about the three-commit stash?

Three-commit stashes are those made by git stash save -a (--all) or git stash save -u (--include-untracked). Applying (or popping) such a stash requires that the files in the third commit not collide with files in the work-tree. In general, this means the work-tree must not only be "clean" according to git status, but also clean in the sense provided by running git clean with appropriate options. In this particular case, even git stash branch can fail sometimes. If it does fail, it leaves the stash un-popped.

For more, see Why does git stash pop say that it could not restore untracked files from stash entry?

torek
  • 448,244
  • 59
  • 642
  • 775
  • How does `git stash` work? *Hopefully my comment can give this answer and its excellent digression enough SEO juice to move it up in search results.* – Andrew Keeton Oct 17 '19 at 22:22
  • Thank you for this answer. Would you mind seeing if you can clarify on my similar question here? https://stackoverflow.com/q/62867041/470749 – Ryan Jul 12 '20 at 22:26
1

git stash list will probably give you what you want.

If you saved your stash with git stash save or git stash push, then git added a comment to the stash which includes the branch name and the specific commit:

$ git stash list
stash@{0}: WIP on master: 039142b Initial commit

Even if you gave your stash a custom name, git still saves the branch name:

$ git stash save "My stash"
Saved working directory and index state On master: My stash

$ git stash list
stash@{0}: On master: My stash
asherber
  • 2,508
  • 1
  • 15
  • 12