There are two flags that you can set for any file-name entry in the index: assume-unchanged
and skip-worktree
. The flags have different intents—assume-unchanged
is meant for extra-slow file systems while skip-worktree
is meant for sparse checkout—but the two generally produce the same effect: the file remains in your index, but when Git goes to compare what's in the index with what's in the work-tree, Git generally just assumes that the file that is in the index matches the file that is, or maybe isn't, in the work-tree.
That's fine as far as it goes, but it doesn't help in some important cases
Remember that the index is perhaps best described as where you build your next commit. As such, it contains a copy1 of every file from the current commit. As long as you're just making new commits based on the current commit, that copy can remain there undisturbed. Your new commit, that you just made by running git commit
, saves another copy (or rather, re-uses it—see footnote 1 again) of that file in the new commit. The new commit becomes the current commit, and your index continues to hold the same copy of the same file. No matter what you do to your work-tree copy of the file, the index copy continues to match the current commit copy.
But as soon as you reach for git checkout
(or the new git switch
) to switch branches, the picture changes. Now you're going to select a different commit to be the current commit. What if this other, different commit holds a different copy of that file?
In order to switch to this other commit on this other branch, Git is going to have to rip the existing copy of the file out of the index and, instead, copy in a new version of that file to the index. That's OK to some extent, but when Git does that, Git will also overwrite your work-tree copy of the file.
We've already established that you have been changing the work-tree copy, without committing the changed file to new commits. That's why you set the flag in the first place: so that the new commits you made would use the old copy in the index, not the current copy in the work-tree. So the work-tree copy doesn't match the committed copies in each of the new commits you have made.
Git is now telling you that by switching commits, Git will destroy the copy you have in your work-tree. You had better save that copy if you want to keep it! You can save that copy any place you'd like. Once you have saved that copy of that file—for instance, cp path/to/file /tmp/save
—you can:
- remove the work-tree file, or
- use
git update-index --no-skip-worktree
to clear the flag, then use git checkout -- path/to/file
or git reset --hard
to overwrite the work-tree copy
so that path/to/file
in your work-tree matches the copy of path/to/file
in your index, or simply doesn't exist at all. Now git checkout <otherbranch>
can safely replace the index copy of path/to/file
with the copy from the target commit, and fill in path/to/file
in your work-tree with the copy from the target commit. So now git checkout
will be happy, and be able to switch to that commit. And, if you want the work-tree copy that was going to be clobbered, well, you've saved it in /tmp/save
, so there you have it.
Note that the difference between these two methods is whether the assume-unchanged
flag is still set in the index copy of the file. I prefer to clear the flag, because there's a tricky corner case. What happens when the commit to which you want to switch—the one at the tip of otherbranch
–doesn't have file path/to/file
in it at all? In this case, the git checkout
operation will remove path/to/file
from the index and from your work-tree. Once the file no longer exists in the index, there is no way for Git to hold an assume-unchanged
flag on it. The file simply isn't there at all; you cannot set a flag bit on an entry that doesn't exist.
(If the target commit does have the file, and you want to set the flag again, you can simply set the flag again.)
1Technically, it contains a reference to the frozen, Git-ified data for the file. This is shared with any other duplicate, so if the file really does match the current commit, there is no extra space used. That is, thinking of it as a copy is subtly wrong—but unless you start using git update-index
with blob hash IDs, thinking of it as a copy works out anyway.