Most of the time when I try to checkout another existing branch, Git doesn't allow me if I have some uncommitted changes on the current branch. So I'll have to commit or stash those changes first.
However, occasionally Git does allow me to checkout another branch without committing or stashing those changes, and it will carry those changes to the branch I checkout.
What is the rule here? Does it matter whether the changes are staged or unstaged? Carrying the changes to another branch doesn't make any sense to me, why does git allow it sometimes? That is, is it helpful in some situations?
You have two choices: stash your changes:
then later to get them back:
or put your changes on a branch so you can get the remote branch and then merge your changes onto it. That's one of the greatest things about git: you can make a branch, commit to it, then fetch other changes on to the branch you were on.
You say it doesn't make any sense, but you are only doing it so you can merge them at will after doing the pull. Obviously your other choice is to commit on your copy of the branch and then do the pull. The presumption is you either don't want to do that (in which case I am puzzled that you don't want a branch) or you are afraid of conflicts.
Preliminary notes
The observation here is that, after you start working in
branch1
(forgetting or not realizing that it would be good to switch to a different branchbranch2
first), you run:Sometimes Git says "OK, you're on branch2 now!" Sometimes, Git says "I can't do that, I'd lose some of your changes."
If Git won't let you do it, you have to commit your changes, to save them somewhere permanent. You may want to use
git stash
to save them; this is one of the things it's designed for. Note thatgit stash save
actually means "Commit all the changes, but on no branch at all, then remove them from where I am now." That makes it possible to switch: you now have no in-progress changes. You can thengit stash apply
them after switching.You can stop reading here, if you like!
If Git won't let you switch, you already have a remedy: use
git stash
orgit commit
; or, if your changes are trivial to re-create, usegit checkout -f
to force it. This answer is all about when Git will let yougit checkout branch2
even though you started making some changes. Why does it work sometimes, and not other times?The rule here is simple in one way, and complicated/hard-to-explain in another:
You may switch branches with uncommitted changes in the work-tree if and only if said switching does not require clobbering those changes.
That is—and please note that this is still simplified; there are some extra-difficult corner cases with staged
git add
s,git rm
s and such—suppose you are onbranch1
. Agit checkout branch2
would have to do this:branch1
and not inbranch2
,1 remove that file.branch2
and not inbranch1
, create that file (with appropriate contents).branch2
is different, update the working tree version.Each of these steps could clobber something in your work-tree:
branch1
; it's "unsafe" if you've made changes.branch2
is "safe" if it does not exist now.2 It's "unsafe" if it does exist now but has the "wrong" contents.branch1
.Creating a new branch (
git checkout -b newbranch
) is always considered "safe": no files will be added, removed, or altered in the work-tree as part of this process, and the index/staging-area is also untouched. (Caveat: it's safe when creating a new branch without changing the new branch's starting-point; but if you add another argument, e.g.,git checkout -b newbranch different-start-point
, this might have to change things, to move todifferent-start-point
. Git will then apply the checkout safety rules as usual.)1This requires that we define what it means for a file to be in a branch, which in turn requires defining the word branch properly. (See also What exactly do we mean by "branch"?) Here, what I really mean is the commit to which the branch-name resolves: a file whose path is
P
is inbranch1
ifgit rev-parse branch1:P
produces a hash. That file is not inbranch1
if you get an error message instead. The existence of pathP
in your index or work-tree is not relevant when answering this particular question. Thus, the secret here is to examine the result ofgit rev-parse
on eachbranch-name:path
. This either fails because the file is "in" at most one branch, or gives us two hash IDs. If the two hash IDs are the same, the file is the same in both branches. No changing is required. If the hash IDs differ, the file is different in the two branches, and must be changed to switch branches.The key notion here is that files in commits are frozen forever. Files you will edit are obviously not frozen. We are, at least initially, looking only at the mismatches between two frozen commits. Unfortunately, we—or Git—also have to deal with files that aren't in the commit you're going to switch away from and are in the commit you're going to switch to. This leads to the remaining complications, since files can also exist in the index and/or in the work-tree, without having to exist these two particular frozen commits we're working with.
2It might be considered "sort-of-safe" if it already exists with the "right contents", so that Git does not have to create it after all. I recall at least some versions of Git allowing this, but testing just now shows it to be considered "unsafe" in Git 1.8.5.4. The same argument would apply to a modified file that happens to be modified to match the to-be-switch-to branch. Again, 1.8.5.4 just says "would be overwritten", though. See the end of the technical notes as well: my memory may be faulty as I don't think the read-tree rules have changed since I first started using Git at version 1.5.something.
Does it matter whether the changes are staged or unstaged?
Yes, in some ways. In particular, you can stage a change, then "de-modify" the work tree file. Here's a file in two branches, that's different in
branch1
andbranch2
:At this point, the working tree file
inboth
matches the one inbranch2
, even though we're onbranch1
. This change is not staged for commit, which is whatgit status --short
shows here:The space-then-M means "modified but not staged" (or more precisely, working-tree copy differs from staged/index copy).
OK, now let's stage the working-tree copy, which we already know also matches the copy in
branch2
.Here the staged-and-working copies both matched what was in
branch2
, so the checkout was allowed.Let's try another step:
The change I made is lost from the staging area now (because checkout writes through the staging area). This is a bit of a corner case. The change is not gone, but the fact that I had staged it, is gone.
Let's stage a third variant of the file, different from either branch-copy, then set the working copy to match the current branch version:
The two
M
s here mean: staged file differs fromHEAD
file, and, working-tree file differs from staged file. The working-tree version does match thebranch1
(akaHEAD
) version:But
git checkout
won't allow the checkout:Let's set the
branch2
version as the working version:Even though the current working copy matches the one in
branch2
, the staged file does not, so agit checkout
would lose that copy, and thegit checkout
is rejected.Technical notes—only for the insanely curious :-)
The underlying implementation mechanism for all of this is Git's index. The index, also called the "staging area", is where you build the next commit: it starts out matching the current commit, i.e., whatever you have checked-out now, and then each time you
git add
a file, you replace the index version with whatever you have in your work-tree.Remember, the work-tree is where you work on your files. Here, they have their normal form, rather than some special only-useful-to-Git form like they do in commits and in the index. So you extract a file from a commit, through the index, and then on into the work-tree. After changing it, you
git add
it to the index. So there are in fact three places for each file: the current commit, the index, and the work-tree.When you run
git checkout branch2
, what Git does underneath the covers is to compare the tip commit ofbranch2
to whatever is in both the current commit and the index now. Any file that matches what's there now, Git can leave alone. It's all untouched. Any file that's the same in both commits, Git can also leave alone—and these are the ones that let you switch branches.Much of Git, including commit-switching, is relatively fast because of this index. What's actually in the index is not each file itself, but rather each file's hash. The copy of the file itself is stored as what Git calls a blob object, in the repository. This is similar to how the files are stored in commits as well: commits don't actually contain the files, they just lead Git to the hash ID of each file. So Git can compare hash IDs—currently 160-bit-long strings—to decide if commits X and Y have the same file or not. It can then compare those hash IDs to the hash ID in the index, too.
This is what leads to all the oddball corner cases above. We have commits X and Y that both have file
path/to/name.txt
, and we have an index entry forpath/to/name.txt
. Maybe all three hashes match. Maybe two of them match and one doesn't. Maybe all three are different. And, we might also haveanother/file.txt
that's only in X or only in Y and is or is not in the index now. Each of these various cases requires its own separate consideration: does Git need to copy the file out from commit to index, or remove it from index, to switch from X to Y? If so, it also has to copy the file to the work-tree, or remove it from the work-tree. And if that's the case, the index and work-tree versions had better match at least one of the committed versions; otherwise Git will be clobbering some data.(The complete rules for all of this are described in, not the
git checkout
documentation as you might expect, but rather thegit read-tree
documentation, under the section titled "Two Tree Merge".)If the new branch contains edits that are different from the current branch for that particular changed file, then it will not allow you to switch branches until the change is committed or stashed. If the changed file is the same on both branches (that is, the committed version of that file), then you can switch freely.
Example:
This goes for untracked files as well as tracked files. Here's an example for an untracked file.
Example:
A good example of why you WOULD want to move between branches while making changes would be if you were performing some experiments on master, wanted to commit them, but not to master just yet...