There are several parts to this answer, because using git stash
can be surprisingly difficult. The way you used it in this case is correct for your use-case but there are many traps here for the unwary and the new-to / not-so-into Git folks.
Stash makes several commits
The main thing to know, from which everything else flows, is this: git stash
makes commits. In fact, it makes at least two commits; if you tell it to (with -u
or -a
) it makes a third commit. All of these commits are on no branch, but they are still commits, and hence behave like commits. The format of these commits is that they look, to other Git commands, like a bizarre kind of merge commit, which means in general that you want to keep other Git commands from looking too closely at them, and mostly use git stash
to deal with them.
The index and work-tree, and git checkout
What git stash
commits are the index and the work-tree. The work-tree is the easiest to explain, by far: Git stores files in an internal, Git-only format, so to work with the files, you need a place where they are stored in the normal computer format. That's the work-tree. You can put, in this work-tree, files that are not going to be stored in Git as well, and this is where the index first comes in.
Git's index has several uses, but this is the main one: it is "where you build the next commit". That is, you start out, in any Git repository, with a current commit that you ran git checkout
to get. (On the first git clone
, Git runs git checkout master
for you. Well, it's usually master
anyway, but it's definitely something checked out.) This picks a branch name, uses it to find the tip commit of the branch, and makes that branch and that commit your current branch and commit. Then it fills the index—which was completely empty, the very first time—from that particular commit, so now the index has in it all the files that are in the current (or HEAD
) commit. When adding these files to the index, Git also copies them into the work-tree, and so now you have the HEAD commit equal to the index which is equal to the work-tree.
If you now git checkout
some other branch/commit, Git changes HEAD
to account for the new branch and tip commit, removes the old files (for the previous HEAD) from the index and work-tree, adds the new files to the index and work-tree, and once again you have the HEAD commit equal to the index which is equal to the work-tree.
Note that in all of this shuffling-about, any file that is in the work-tree that is not in the index, goes untouched. These are your untracked files, and that's what it means for a file to be untracked: A file is tracked if and only if it is in the index. (This only has a little to do with git stash
itself, but it's crucial to using Git and .gitignore
, because .gitignore
has no effect on a tracked file.)
To make a new commit yourself, you first modify a file in the work-tree, then use git add
to copy the new version into the index. This makes the new file ready to be committed, with the data it has right at the time you run git add
. This leaves all the existing index files the way they are, so the new commit will still have all the other files unchanged. To add a file that isn't already in the index, you create it in the work-tree and again run git add
. The copies the file into the index, just as before, but this time it's not overwriting an existing copy. To delete a file, you run git rm
, which removes it from both the index and the work-tree.
This process of adding (overwriting existing or adding new) files to the index, and removing files from both index-and-work-tree, is what we mean when we say staging files for commit. This is why the index is also called "the staging area": it's where we copy the updated work-tree files into, to get them staged. Note that unless you git rm
everything, the index itself is never actually empty: it's just that once you git commit
the index, the index and the new commit you made, now match. (This makes the --allow-empty
flag to git commit
a bit misleading.)
Back to git stash
, specifically the "save new stash" sub-command
Again, git stash save
makes (at least) two commits: first, it commits the index itself—which is really easy since that's how Git always makes commits—and then it makes a second commit that consists, in effect, of git add
ing all the tracked work-tree files, i.e., copying them into the index, and then committing again. For internal reasons, though, it makes this second commit as a merge commit, merging HEAD
and the index commit it just made. We don't have to care as long as we use git stash
to deal with this weird merge. It's only if and when we step outside git stash
that we suddenly have to care about it.
After git stash
makes those commits, it mostly does the equivalent of git reset --hard HEAD
, although here again there are flag options to change this. (For much—perhaps too much—more about this process, see How to recover from "git stash save --all"?) What git reset --hard HEAD
does is to re-set (hence git reset
) three things:
It moves (i.e., re-sets) the current branch from its current commit to the specified new commit. Because the specified new commit is HEAD
, which is the current commit, this is like trying to drive from your kitchen to your kitchen: you may scurry around for a while, but you just end up exactly where you started.
It re-sets the index. That is, it makes the index match the HEAD
commit. This un-stages all your staged changes, un-removing git rm
-ed files, un-adding any totally-new-files, and recovering from the HEAD
commit any files you modified and then staged.
It re-sets the work-tree. That is, it makes all tracked files in the work-tree match their versions in the HEAD commit and (now) the index.
As a kind of mental short cut, though, you can think of this as "ream out the index and work-tree". The stash
code just saved them both, as commits, so it's now safe to clean them out. Now the index and work-tree both look like they would if you had just freshly run git checkout
on the current commit. This is why you are now ready to use git merge
or git rebase
(whether or not you run it from git pull
—remember, git pull
is just git fetch
followed by merge or rebase, in all of these cases).
Applying the stash
The process of applying (or "popping") a stash can be more complicated than the process of saving it, but in practice, it tends to be simple. Well, except when it goes wrong, anyway. You just get to a (usually clean) commit-and-index-and-work-tree state as before, and run:
git stash apply
What this does is run a couple of git diff
commands, to find out:
- What's the difference between the saved index, and the commit that was current when
git stash save
saved that index?
- What's the difference between the saved work-tree, and the commit that was current when
git stash save
saved that work-tree?
Usually, for each file, there's at most one difference (either via the saved index, or via the saved work-tree): you either staged the file, so the difference shows up in the saved index, or you didn't, so any changes you made show up in the saved work-tree (if there are no changes there's no difference at all). Git then kind of smashes these all together, for each file, and puts all the changes into your current work-tree. Your current index goes un-affected, by default, although there are fancier ways to git stash apply
.
Once you are satisfied that the stash has applied correctly, you can run git stash drop
. This discards the two saved commits (and if git stash save
"pushed" an earlier stash into stash@{1}
, effectively "pops" the rest back down so that the earlier stash is now stash@{0}
, or just stash
).
If you're sure you want to toss the stash after applying it, without first checking for correctness, you can use git stash pop
. This literally just turns into git stash apply && git stash drop
internally. (Of course, it passes the specific stash through, if you're using git stash pop stash@{n}
.)
Some hard cases, like XML files
The process of applying each change—each git diff
from commit-that-was-HEAD-at-the-time, to saved index or saved work-tree—uses Git's full merge powers. This is a simple(ish) mechanical process, but it works surprisingly well for most files. XML files, unfortunately, tend to defeat Git's not-exactly-super powers.
In this case, you may want to do exactly what you did here.
Because git stash save
simply makes two commits (that are not on a branch), you can use all the usual Git tools to deal with these two commits. But once you do so, you must be careful, because the work-tree commit looks like a merge commit. (Well, in fact, it is a merge commit.) Fortunately, git show <commit>:<path>
doesn't care if the commit is a merge: it just extracts the saved version of the file.
Each stash
name or reflog name points to the saved work-tree commit. Hence:
git show stash:path/to/file.xml
prints the saved work-tree contents of that file. Redirecting that to a new file in your work-tree gives you a chance to examine the saved contents. So this is a fine thing to do.
Watch out, though, for git show stash
. For those of us with dyslexia,1 it's really easy to use this instead of git stash show
. But git show <commit>
does care if the <commit>
is a merge commit. While git stash show
knows to treat the work-tree commit as a separate, non-merge-y commit, git show
—which sees stash
as a commit ID, which then works but gets you the work-tree commit—tries to treat it as a merge commit. It tries to show a combined diff, which often just ends up showing nothing at all. (The most confusing case occurs when it does show something, but not all of what you stashed, which happens when you first staged a file, then modified it again in the work-tree.)
TL;DR summary (if it's not too late): git show stash:path/to/file
is OK, but git show stash
is always a mistake. Mostly you want git stash apply
, but for odd cases, git show stash:path/to/file
will get you the saved work-tree version. And, for really complicated cases, see my other longer answer and consider using git stash branch
.
1Dyslexics of the world untie!