Ortomala Lokni Ortomala Lokni - 3 months ago 20
Git Question

How to revert a git checkout masTER?

I was on a test branch and by mistake I run

git checkout masTER


It renames my master branch to masTER. When I run
git branch
, I obtain:

masTER
test


If I run
git checkout test
and then
git checkout master
. The master branch doesn't retrieve its original capitalization.

How can I revert this mistake, and why does this happen?

P.S: I use git version 2.7.4 (Apple Git-66)

P.S2: I'm using OS X 10.11.6 which use Journaled HFS+ and is case-insensitive by default. If I run:

touch abc
touch abC


Running
ls ab*
gives only

abc

Answer

Defining the problem

This problem occurs on any case-preserving but case-insensitive file system. By default, then, it occurs on Macs and Windows boxes, and not on Linux and the like.

Git itself is case-sensitive, pretty much everywhere. A file named master.txt is independent of one named masTER.txt, and therefore a Git tree object will keep the two as separate items, and Git's index (also known as the cache or the staging-area) considers these separate files. There is a core.ignoreCase setting in git config meant to help it recognize when the underlying OS disagrees, but in general Git thinks of these as different.

Since these file names are stored inside files—repository objects of type tree, stored in .git/objects/, and Git's index, stored in .git/index1—and not merely handed over to the OS to store, they can be, and therefore are, case-sensitive, even if the OS is not.2

This is true for branch names, and remote-tracking branch and tag names, as well. Since these names are kept in the file .git/packed-refs, they can be case-sensitive. (Git also stores its idea of your current branch in .git/HEAD, as a text string, which is therefore case-sensitive.) But these names are not always kept in .git/packed-refs. In fact, they're often not in .git/packed-refs at all. An active branch name—one that is being modified—winds up stored in an individual file in .git/refs/heads/, such as .git/refs/heads/master. An active remote-tracking branch name winds up in .git/refs/remotes/, e.g., .git/refs/remote/origin/master.

If the OS is case-insensitive, this means that branches named master and masTER are different when they're packed, but become the same once they become active!

It gets worse though. A name that was inactive gets packed: moved into .git/packed-refs. If the name now becomes active, it appears in both .git/packed-refs and (assuming it's a branch name) .git/refs/heads/. This means that an inactive name is case-sensitive, and an active name is case-insensitive, and you can get both happening at the same time so that you have a masTER and a master that are different (because both appear in .git/packed-refs where we can tell them apart) and, simultaneously, the same! What this means—how Git behaves when you're in this bizarre Schrödinger's-cat3 situation—is not at all clear. Obviously it's a bad idea, though. (I managed to put my test repo into this state where I had both master and masTER by doing git pack-refs --all and then git checkout -b masTER. I did not push it any further though.)

Fixing it

The OSes (see footnote 2 again) presenting the problem today are case-insensitive but case-preserving. This means that once you have a file named masTER, attempts to use or create a file named master, Master, MASTER, mAsTeR, and so on, all refer instead to the existing file masTER.4

Some of these OSes allow you to rename the file with just a case-change (mv masTER master for instance), so git branch -m masTER master might do the trick, if Git would just issue an OS-level "rename" operation. Alas, in practice Git checks first, and finds that it can access a branch named master (which is of course the file named masTER):

$ git branch -m masTER master
fatal: A branch named 'master' already exists.

Hence, the fix is to rename the branch twice. First we move it to a name that the OS cannot find, that does not match any existing case-folded branch names. You can just pick any unlikely name and hope it is not in use, or you can run git branch (or git for-each-ref refs/heads) to check whether the unlikely name you just picked is in fact in use. In any case, if you rename the current (wrong-case) branch to this new unused name that does not, even under silly case-folding rules, match some existing name, that will, as a side effect, delete the old, wrong-case-but-OS-insists-on-matching-it name entirely:

$ git branch -m masTER tmp

Now that the old "wrong-case-but" name is thoroughly eradicated, now you can rename the branch back, with the correct case:

$ git branch -m tmp master

Note that this also takes care of stripping out the wrong-case name from .git/packed-refs, if it got in there.


1These paths are just defaults. The new extra-worktree feature changes where some of these files are found, sometimes, and Git has environment variables that can override some of them.

2Technically, modern OSes do case-sensitivity on a per-file-system basis, rather than globally. So where this says "if the OS is case-X" you can mentally substitute in "if the file system I'm using is case-X".

They may get it wrong. Consider German, where the word "straße" (street) is written this way in lowercase, but in uppercase, becomes "STRASSE". If we compare one character at a time with a case-insensitive character comparison, we have a problem when we hit the eszet ß as it converts to not one but two uppercase S-es.

This Python3 session shows that MacOS does indeed not believe that ß = SS:

>>> s
'straße'
>>> with open(s, 'w') as stream:
...     stream.write('street\n')
... 
7
>>> os.listdir()[10]
'file_абвгде'          # oops, that's one I was using for testing proftpd
>>> os.listdir()[30]   # should clean out my tmp dir more often ...
'straße'
>>> open('STRASSE')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'STRASSE'
>>> open('STRAßE').read()
'street\n'

It's things like this that sometimes convince me that computers should never interact with humans, and vice versa. :-)

3Honey, what did you do to the cat? It looks half-dead! —Schrödinger's Wife

4Older case-insensitive OSes—and at the time, this really was entire OSes—simply folded all names to one case, invariably uppercase. Hence asking to create masTER actually created MASTER instead. This is at least as wrong as case-preserving-yet-folding. It is, however, less confusing: you can at least tell just how the OS will mangle your data. The choice of case was rather shouty, of course. Admittedly, in the days of the old uppercase-only Teletypes, they had an excuse.

Comments