"TL;DR" section: beware of just blindly doing git stash save && git checkout ... && git stash pop
.
There are actually several pieces you can tease apart, especially when using git's command-line interface.
In particular, the "current branch", if any, is simply an item recorded in a file (the file containing the HEAD
reference, .git/HEAD
). Looking at the raw contents of that file, you will generally see ref: refs/heads/master
and the like. (In "detached HEAD" mode you will see a raw SHA-1 instead.) There are low-level git commands that will update HEAD
without doing anything else.
However, most people mostly switch branches using git checkout
, which—besides being the right way :-) to do it—has a number of protections built in. Mainly, it will refuse to switch branches if you have work-tree or "index" (AKA cache) modifications that would be lost by such a switch. Let's say you're on branch A
and you ask to switch to branch B
. The checkout process must:
- get a list of all files that exist in the tip commit of branch
A
- get a list of all files that exist in the tip commit of branch
B
- for files in the first list that are not in the second, remove those files from the work-dir
- for files in the second list that are not in the first, add those files to the work-dir
- for files that are in both, replace the work-dir contents if (and only if) the files differ
Furthermore, the check-out is done by "writing through" the cache: if file F
is different in A
and B
, the contents of the B
version are first to be copied into the index/cache, and then written to the work-directory.
If you've staged (with git add
) some modification to file F
, or you have some un-staged modification to F
in your work directory, this checkout process would overwrite those staged or un-staged changes, so git checkout
stops with an error message. If file F
is to be removed, that too would overwrite (or perhaps more accurately, remove) your changes, so again the checkout
stops.
On the other hand, if file F
is the same in both commits, the checkout
can proceed: it just leaves the un-committed changes staged or unstaged. This is why you can sometimes, but not always, simply git checkout
the branch you wanted to be working on.
Git's "stash" (as in git stash
) is, as you saw, independent of branches. The key concept here is that each stash—you can have more than one active at a time—is actually a commit (or more precisely, a set of commits: two or three, depending on just what you stash). Before you object that commits are made on branches, though, we have to make another couple of distinctions. Specifically, the word "branch" refers to two or three different things, in git.
Commits always (necessarily) go into the "commit graph", since the graph is simply the thing formed by all commits and their edges. To the extent that the word "branch" means "a part of the commit graph", these stash commits are on branches. But the word "branch" also refers to the names that identify a branch tip commit, and here, these stash commits do not advance the branch-tip. (See this post, also by Jubobs, for more details on the multiple meanings of "branch".)
The git stash
command looks at the current index/cache and work-tree state, and makes new commits from them if—this "if" turns out to be quite important—if they have un-saved staged or unstaged changes. One of these new commits (from which the others can be found) is saved under the special reference-name stash
. None of the new commits are added to the current branch, though, so they're not on any named branch. In that sense, they're independent of the current branch—but because they are commits, they have parent commit IDs, and in that particular sense, they're attached directly to the commit that was in effect when the save
was done.
What this means for you, the end user, is often "nothing": you probably don't care, and you don't have to care. However, if you ever use git stash branch
to turn a stash into a branch, it means that the new branch will fork off from the commit the stash is attached-to. (This, as it turns out, is usually exactly what you want.)
One risk with defining an alias or macro that does git stash save && git checkout ... && git stash pop
is that the first step, git stash save
, may do nothing.
If it does nothing—if it pushes no new stash on the "stash stack"—then it succeeds anyway, and your alias-or-macro will go on to check out some other branch, and then (try to) pop a stash off the stash stack.
If there's another (different) stash on that stack, that you meant to use somewhere else, well, you just attempted to pop it into the branch you just switched-to.
Note that there are two stacked "if"s here, two conditions that must hold, for this particular bug to bite:
- you need to have nothing to stash, and
- you need to have some existing stash you don't want popped.
One way to handle this problem is to use a script, not just a simple git alias, to do the branch-switch-with-stash sequence. In the script, you run git stash
as usual, but before and after the stash, you check the SHA-1 that the stash
reference resolves to, if any. If this changes, the git stash save
saved something, so there is something to pop, and you can go on to do the checkout-and-pop sequence. If it does not change—if there was no stash before and after, or if the stash at the top of the stack is still at the top of the stack—then there is nothing to pop, and you should just do a checkout.
There's another bug that can bite you here; see this answer to a somewhat different question, which includes a bit of shell code expressing the above "pop only if save
actually pushed something" rule.