I want switch branches and have git always set the working directory contents to reflect the state of the branch I\'m switching to. I\'m experiencing the behaviors sta
Uncommitted changes remain in your worktree or index until you tell git where to commit them.
git add
, git checkout
and git reset
all have a --patch
option. The workflow is, doing what needs doing and recording it where it needs recording are separate tasks. Often enough you find yourself doing effectively drive-by bugfixing, where you find a complete mess someoneusually me left and the issue is, there's doing the work, and doing the paperwork, but you're staring right straight at the work and know exactly what needs doing.
The git way1 is, Do It. Get it all correct in your worktree (this may involve a bit of exploratory branching itself)
That's the important part. That's what makes professional, publishable work. What trips people up is, git's meant at least as much for the messy part that comes before that.
1hahaha he said "the git way", as if there were only onelol
As jthill said in comments, you will need to figure out what to do with untracked and/or ignored files.
Moreover, you must not be in the middle of a conflicted merge.
There is a simple solution to all of this (use a different work-tree, perhaps from a different clone or perhaps from git worktree
if your Git is new enough). If that's not desirable for some reason, though, here are some things to consider. Let's take a somewhat hypothetical example, but show some real-world problems. Suppose you're in repo project
, currently on branch dev-a
:
$ cd project
$ git status
On branch dev-a
You have unmerged paths.
(fix conflicts and run "git commit") ...
In this case, you are really quite stuck. Anything you do here will lose your partially-merged state. If there is nothing important in the merged state, you can run git merge --abort
to stop merging, and throw out the conflicted index, and now we're back to the previous case. Let's see if Git thinks everything is clean.
$ git merge --abort
$ git status
On branch dev-a
nothing to commit, working tree clean
Apparently everything is clean. But wait!
$ cat foo
I am a foo
$ git checkout dev-b
Switched to branch 'dev-b'
$ git status
On branch dev-b
Untracked files:
(use "git add <file>..." to include in what will be committed)
foo
nothing added to commit but untracked files present (use "git add" to track)
Was everything clean? Well, file foo
is ignored in branch dev-a
, but not in branch dev-b
, where it now shows up as "untracked". We can use git status --ignored
from dev-a
to see it:
$ git checkout dev-a
Switched to branch 'dev-a'
$ git status --short --ignored
!! foo
Untracked and/or ignored files will now show up as UU
or !!
(in the short output—the long output is as already seen).
If you wish to simply remove ignored files, you can use git clean
with the -x
option (in addition to any usual options).
If you want to save them first, you may use git stash -a
. The stash code will make three commits, instead of the usual two; the third commit will hold the untracked and ignored files. (Note that git stash -u
saves only the untracked, not the ignored, files, in the third commit.) After saving the files, the stash code will remove them, leaving you with a clean (as in git clean -fdx
) work-tree.
Note that a file that is ignored (and therefore not untracked) in one branch, such as dev-a
, can be non-ignored, and therefor either untracked or tracked (but not both) in another branch, such as dev-b
and dev-c
. If file foo
is ignored in dev-a
and you switch to dev-b
it becomes untracked, as we saw. But what if it's ignored in dev-a
and tracked in dev-c
?
$ git status --short --ignored
!! foo
$ git checkout dev-c
Switched to branch 'dev-c'
$ cat foo
I am a foo
$ git checkout dev-a
Switched to branch 'dev-a'
$ cat foo
cat: foo: No such file or directory
File foo
is tracked in dev-c
and ignored in dev-a
, so it gets removed when we switch from dev-a
to dev-c
(because that's what changing from the tip commit of dev-c
to dev-a
requires):
$ git diff --name-status dev-c dev-a
A .gitignore
D foo
There's another very tricky case here. Remember that foo
is ignored in (the tip commit of) dev-a
, untracked in dev-b
, and consists of I am a foo\n
in dev-c
. As we just saw, switching from dev-a
to dev-c
extracted the version from dev-c
. This is true regardless of what we put in it:
$ git rev-parse --abbrev-rev HEAD
dev-a
$ echo 'If the foo s.its, wear it' > foo
$ git checkout dev-c
Switched to branch 'dev-c'
$ cat foo
I am a foo
Let's get back to dev-a
and put our twisted Foo Bird joke back again, and this time, let's step through branch (and tip commit) dev-b
, where file foo
is untracked, rather than ignored:
$ git checkout dev-a
Switched to branch 'dev-a'
$ echo 'If the foo s.its, wear it' > foo
$ git checkout dev-b
$ git status --short
?? foo
$ git checkout dev-c
error: The following untracked working tree files would be overwritten by checkout:
foo
Please move or remove them before you switch branches.
Aborting
This is because an ignored file is also a clobberable (clobber-worthy?) file, but an untracked file is not. There is, in Git's little mind, no notion of a path-name that should not be complained-about during git status
(i.e., ignored for git status
, and skipped over when adding "all" files in some directory), yet is also precious (must never be clobbered by git checkout
).
All of these are complicated even further by files marked, in the index, with the --skip-worktree
or --assume-unchanged
flags. I have not tested out these additional corner cases—but all are avoided by using a separate work-tree, rather than trying to cram everything into a single work-tree. (The reason is that a separate work-tree also implies a separate index. The git worktree
code does this fairly well, or you can simply clone the repository locally, which is smart enough to share files when possible.)