5

I added a file with git update-index --skip-worktree which works great. But now I can't change branch:

"Cannot complete the operation because of existing changes to the following file" and it shows the file that I updated the index on.

I had one commit where I changed that file. After that I realized I should have updated the index so I made the changes to the index and then reverted that commit. Then I made the changes again. Now I am in this situation...

MuhKuh
  • 356
  • 1
  • 16
  • 1
    You need to revert the flag to be able to switch `git update-index --no-skip-worktree` and surely stash the changes. – Philippe Dec 03 '19 at 14:36
  • 1
    Why would I need to revert the flag? Settings the flag is what I want. So I have to set the flag again after switching? – MuhKuh Dec 03 '19 at 16:01

1 Answers1

2

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 otherbranchdoesn'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.

torek
  • 448,244
  • 59
  • 642
  • 775