4

I've started using SourceTree and Git recently, but I am still confused about stashing/branching and my working copy files. I've been having trouble finding anything out there that clarifies things for me.

My confusion is with regards to the working copy of files in relation to branches and stashes. The way I am expecting things to work is that my working copy of files will also be linked to the branch. If I am to switch branches, it will automatically stash my working copy before switching to the new branch. If I am to switch back to the previous branch, it will again stash the current working copy files and restore the ones from the previous branch.

After working with SourceTree and Git for a little while now, it appears pretty clear to me that this is not how it works and that your working copy files are completely independent from your branches, as are stashes. If you switch to another branch, your choices are to manually stash your working copy files, discard the changes, or bring your working files with you to the new branch.

So, what I am wondering is, what is the ideal workflow for this? Say I am developing two features simultaneously in two different branches and want to constantly jump back and forth. Do I need to remember to stash my working copy files before every time I switch or is there a better approach to this?

Mathieson
  • 1,194
  • 2
  • 13
  • 28
  • *If I am to switch branches, it will automatically stash my working copy before switching to the new branch.* Nope! If you have uncommitted changes that conflict with the branch you're checking out, Git won't let you do it. You'll need to stash them "manually"; Git doesn't do it automatically for you. – jub0bs Dec 30 '14 at 00:07
  • Ok good to know that I'm not just missing something. I guess the popup from SourceTree asking if you want to discard local changes should also act as a reminder to stash them before switching. – Mathieson Dec 30 '14 at 00:13
  • I believe you would have a much better experience if you used Git from the command line, but that's just my opinion. In particular, I'm not sure whether SourceTree allows you to define aliases / macros. – jub0bs Dec 30 '14 at 00:15
  • Was not aware of aliases or macros. Doing some searching now and haven't been able to find anything about them in SourceTree yet. Will probably start looking into command line to learn more about these. Thanks for the suggestion. – Mathieson Dec 30 '14 at 00:24

2 Answers2

4

"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.

Community
  • 1
  • 1
torek
  • 448,244
  • 59
  • 642
  • 775
  • Nice read, as always, but you're throwing the poor OP (who only knows Git through a GUI) in the deep end! Nice bit about how `git stash save` can bite you. I hadn't realized that. – jub0bs Dec 30 '14 at 01:17
  • 1
    That's why I added the "TL;DR" bit at the top :-) – torek Dec 30 '14 at 01:23
  • I would love to copy paste a ready made solution which check the SHA1 before/after the stash save, hint: https://stackoverflow.com/questions/14135233/get-sha1-of-latest-remote-commit – aka.nice Dec 29 '18 at 15:36
  • @aka.nice: There are several ways to do that. The one that works with the oldest versions of Git is to use `git rev-parse refs/stash` before and after `git stash save`. The rev-parse will either fail (no stash exists) or succeed and print a hash ID (stash exists and the hash ID is that of the latest stash). If it fails before but succeeds after, there is a new stash that is the only one. If it fails before and fails after, there is no stash. If it succeeds both times, the stash changed—i.e., the save succeeded—if and only if the two hash IDs differ. – torek Dec 29 '18 at 23:04
  • If you represent "fail to parse" as an empty string, you can simply test whether the two results are different, hence, this shell fragment: `oldstash=$(git rev-parse -q --verify refs/stash); git stash save; newstash=$(git rev-parse -q --verify refs/stash)` which sets the old and new stash IDs to "" or a valid hash ID. Then: `if [ "$oldhash" != "$newhash" ]; then ...; fi` tests whether a stash was created. – torek Dec 29 '18 at 23:07
1

If I am to switch branches, it will automatically stash my working copy before switching to the new branch.

Nope! Git doesn't automatically stash local changes for you. Also, if you have uncommitted changes that conflict with the branch you're checking out, Git won't let you check out the branch in question. You'll need to either discard or stash them "manually" before checking out that other branch.

it appears pretty clear to me that [...] your working copy files are completely independent from your branches, as are stashes.

Yes, and there are good reasons for that. In particular, one use case for stashing is when you start doing changes while the "wrong" branch is checked out. You can then get out of trouble by

  • stashing your local changes (thereby landing in a clean working state),
  • checking out the "correct" branch,
  • popping that stash to recover your local changes.

Say I am developing two features simultaneously in two different branches and want to constantly jump back and forth. Do I need to remember to stash my working copy files before every time I switch or is there a better approach to this?

Yes, stashing your changes before switching to another branch, then popping a previous stash, would be the normal workflow. If you switch branches often, that can indeed be tedious, but if you use Git from the command line, you can define aliases in order to abstract some of that complexity away.

I've never used SourceTree myself, but I can imagine how that stashing/checking out/popping can involve quite a lot of tedious mouse clicks. Apparently, though, SourceTree introduced a mechanism called "Custom Actions" that allow you to define your own commands, including Git commands. You might want to look into it...

jub0bs
  • 60,866
  • 25
  • 183
  • 186