Howiecamp Howiecamp - 1 month ago 7
Git Question

How can I checkout a branch while ensuring that none of my current branch's changes are carried over?

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 stated here https://web.archive.org/web/20160331103129/http://www.gitguys.com/topics/switching-branches-without-committing - git is making a decision** on whether to carry certain files from the working directory associated with my current branch to the branch I switch to.

** This seems to conflict with what's stated here https://www.atlassian.com/git/tutorials/using-branches: "Checking out a branch updates the files in the working directory to match the version stored in that branch..."

In some cases I don't want git to make a decision - I just want Git to make the working directory match the state of the new branch. However, I don't want to lose any changes. I'm willing to commit or stash prior.

Possible?

Answer

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

Comments