Search code examples
gitgit-rebasegit-squash

Why error switching branches after squashing? Nothing to commit


I just squashed my remote commits, then forced-pushed them to the remote. A git status shows no changes. Why then would I get this error when trying to checkout the develop branch?

error: Your local changes to the following files would be overwritten by checkout:
[file here]
Please commit your changes or stash them before you switch branches.
Aborting

Here is what was done:

git rebase -i origin/feature/EX-1576~14 feature/EX-1576
git push --force origin feature/EX-1576

And git status shows nothing:

$ git status
On branch feature/EX-1576
Your branch is up to date with 'origin/feature/EX-1576'.

nothing to commit, working tree clean

It's my understanding that the error occurs when you have local changes, but I don't have local changes. I simply want to do this:

git checkout develop

Solution

  • TL;DR

    Follow Git's advice: for each file that it names, move that file somewhere else (out of the way, perhaps out of the project entirely), or commit it. Then do the checkout, and see what file(s) you got that replaced those files, and decide whether to keep the replacements, or to use the saved copies you made before the checkout.

    Be careful with git update-index --assume-unchanged or git update-index --skip-worktree: these work well for some cases, but set you up for this particular trap.

    Since you are on Windows, which defaults to conflating files named (e.g.) readme with other different files named README—Windows can't store both; it just clobbers one of them—be careful with case-sensitive file names, usually made by some Linux programmer. :-)

    Long

    It's my understanding that the error occurs when you have local changes ...

    That's not really right. You get that error when the operation—in this case, git checkout—would overwrite some state.

    Git isn't about changes at all. Git is mostly about commits, and commits save state—a snapshot of all of your files, along with your metadata: your name and email address, the time you made the commit, and your log message as to why you made the commit, for instance. (Included in this metadata is another critical item, the parent commit hash ID, but we can ignore that for this particular problem.)

    The difference between state and changes is like talking about the weather: saying it's warmer today than yesterday tells you one thing, but not everything, about the temperature. Saying that it was 15˚C (59˚F) yesterday, and is 20 / 68 today, tells you everything about the temperature. (Well, about this one temperature, anyway.) Note that it took two states to come up with the change: we have to subtract yesterday's temperature from today's to see how much warmer or colder it might be.

    Anyway, commits store state: a full, complete copy of every file that was committed, as of the time it was committed. This copy actually comes out of Git's index, but we get to ignore that fine distinction for the moment. It's about to crop up in a moment, though. So each commit is very much independent of every other commit.

    Your work-tree, on the other hand, is not something Git saves (at all, really, because of the index). You use it to work on your files, because the committed copies are in a special, frozen, compressed (sometimes very compressed), Git-only format. To make these useful, Git needs to expand them out into ordinary-format files, that you can use and change if you like. Those expanded copies go in your work-tree.

    Now, a thing about the work-tree is that it's allowed to contain files that you won't commit. These are what Git calls untracked files. Normally, if there is a file in your work-tree that's is untracked—that won't be committed—Git will complain about that file. You can make Git shut up about it by listing the untracked file in .gitignore, but this is trickier than it looks. This is where Git smacks you in the face with the existence of the index, again.

    The index is a weird and wonderful, but also obnoxious, thing that is pretty much unique to Git. In between the commits, which store files in a frozen Git-only compressed form, and the work-tree, which lets you work on your files, Git puts a third copy of every file. The index copy of each file is in the special Git-only format, but instead of being frozen, it's merely ready to freeze: kind of slushy, if you will. The point is that you can change this copy, and that's what git add does: it copies a file from the work-tree, into the index.

    It's actually the presence of the index copy that determines whether or not a file is tracked. If the file is in the index, it's tracked; if not, it's untracked. Listing a file in .gitignore means: if it's not in the index, and is in the work-tree, don't complain. But it has a second side effect, which is: it gives Git permission to destroy the file, in some cases.

    Filename case issues

    Linux programmers happily write and commit two different files, one named README and other named readme or Readme. Or they do the same with header files: ip.h and IP.h (in older Linux kernel trees). When someone using a Mac or a Windows box tries to work with these commits, they get bitten by the fact that the work-tree on these systems can't put both files into place. (Git's index handles it just fine, because the index is actually a file, .git/index.)

    If you are switching from a commit that has a file named README to one that has Readme, or that has both, Git will sometimes get a little discombobulated by this, and not know what to do. (Git needs to be smarter about this, someday.)

    assume-unchanged and skip-worktree

    In any case, suppose a file is in the index. If you change the work-tree copy, Git will tell you that you have a modified tracked file. If you don't want Git to keep reminding you about this, you can use git update-index --assume-unchanged or git update-index --skip-worktree to mark that file specially.

    When you do this—and I think you probably did—Git stops comparing the index copy of the file to the work-tree copy, for git status commands, and does not copy the work-tree copy of the file over top of the index copy, for git add commands. This means that you can take a configuration file, modify it for some reason, and yet have new commits—which use the index copy of the file—store the original version of the file, the one that came out of the commit and went into your work-tree before you set the assume-unchanged or skip-worktree bit.

    But git checkout must, when it goes to switch to some other commit, replace the index copy of that file with the (different) committed copy in the commit you're going to switch to. When this happens, Git will not only update the index copy, it will also overwrite the work-tree copy. So if you have a file marked with either of these two bits, you can get that error when you use git checkout.

    Is this a problem? Maybe so, maybe not. If you force git checkout to check out that other commit, Git will overwrite the index entry with the file from the other commit, and replace the work-tree copy of that file with the one from the other commit. It's up to you to decide whether this is OK, and if not, whether you need to move the file out of the way first, or clear those bits and go ahead and add-and-commit the file.

    There are also corner cases with half-ignored files

    Suppose, on the other hand, you didn't set those index bits with git update-index. You could still have an ordinary, untracked file, perhaps even one listed in a .gitignore to keep Git quiet about it. But some other commit might have (a different version of) that same file, and if you have Git switch to that commit, Git will have to replace your untracked work-tree file with the version out of the commit where it's a tracked file.

    In this case, git checkout will sometimes—but not always—also complain. Usually it will say that the checkout would overwrite an untracked file. If the file is listed in .gitignore, this will give some parts of Git permission to clobber it. Fortunately git checkout is usually pretty careful about these things.