Change file name from lowercase to uppercase. use git config --local core.ignorecase false, but two files in the same name but different case

Viewed 68

I changed file name from lowercase to uppercase, then I tried to merge.

It was failed, so I add

git config --local core.ignorecase false

Then it became same name but two files in gitlab (one with uppercase, one with lowercase).

If I clone the project, it will be all lowercase.

I tried to delete the lowercase one in gitlab, but if I pull again, the file will be empty.

Not sure how to fix it.

1 Answers

The best way to fix this problem is to use a system in which lowercase and uppercase are distinct. For instance, if you are stuck with Windows, fire up WSL or use VirtualBox or similar to create a Linux system, clone the repository into this other OS, fix things up, commit, and push new commit. (You can then shut down the WSL or VirtualBox instance.)

Long-ish: what's going on

You've misused core.ignoreCase: you're lying to Git, and Git got its revenge.

There's a key concept here that's a little tricky, and unfortunately, a lot of Git documentation is unclear and a lot of Git tutorials and introductory materials are not great or even downright bad, which can lead you down the garden path here. Here is what you need to know:

  • Any given Git repository stores commits (and names, such as branch and tag names, that help you and Git find the commits, but here we'll concentrate specifically on the commits).

  • Commits store files—but they do so in a special, weird, exclusive-to-Git format. Commits don't store directories (folders), only files; each file has a name, and that name consists of any arbitrary byte sequence with only a few constraints: specifically, the name must not start with a (forward) slash and cannot have two adjacent forward slashes, and it cannot contain a NUL byte (a b'\0' in Python-style encoding). Two different byte sequences represent different file names, so path/to/file and path/TO/FILE are two different files; path/TO/file is a third different file, and so on. Note that there are no folder names here: path/to/file is a file name, that merely happens to contain things that work like folder names when you're getting your real work done, rather than poking around with Git.

  • To check out a commit (with git checkout or git switch), Git will extract the files from the commit into a what Git calls a working tree. Here the files will be ordinary files on your ordinary computer so that ordinary programs can do ordinary things with them.

  • Git actually builds new commits from what Git calls its index or staging area (or, sometimes, cache). This thing holds files, in the same format that Git uses for commits, so the files in the index have long names (path/to/file) that are case-sensitive, regardless of whether your system is case-sensitive or not.

Hence a Git repository, which stores commits which store files, can contain a commit that has two files that differ only in case, such as readme.md and README.md. This is always true: no setting you can set in Git can stop this from being true.

Most operating systems and/or file systems have constraints on how files can appear. Not all OSes and file systems are the same here: in particular, Windows, after breaking a long path/to/file.ext string into components path, to, and file.ext (and then expressing that as path\to\file.ext later!) forbids the file part from being named aux. Try to create an aux or aux.h file for a C or C++ program, or an aux.py file for a Python program, and see what happens. This works fine on Linux and macOS, but not on Windows.1

Both Windows and macOS will, by default in most file systems, retain the case of a newly created file or folder, so that if you make a new file named README.md, it's really named README.md. But both systems will, from then on, treat attempts to create or use readme.md (in all lowercase) as requests to use the existing README.md file. That is, these operating systems and/or file systems ignore the case you use in favor of the case of some existing file or folder.

This means that, should you have a commit that has both README.md and readme.md in it, checking out this commit can only extract one of these two files. Whichever file name Git creates first will be the name you will see in your working tree. Let's say, for concreteness, that Git creates README.md first. The second file will simply overwrite the first one, leaving you with just a single file named README.md, containing the readme.md file's content.

What core.ignoreCase (which, as a configuration variable, ignores case so that you can set core.ignorecase or core.IGNORECASE if you like) is for is to tell Git how the system behaves. Changing it won't change how the system behaves! It's a descriptive setting, not a prescriptive setting.

It's occasionally (though rarely) useful to change the setting temporarily to trick Git. You should never have to do this, in an ideal world in which Git is perfect, but that world simply does not (and never will) exist. So you'll find the occasional StackOverflow answer that has you temporarily change core.ignoreCase and do something. Be sure to change it back when you're done though because Git believes the setting. If it's set wrong, Git will misbehave. The answer that says "change it" is deliberately lying to Git to get it to misbehave in a way that turned out to be useful that one time, and maybe will be again, as long as you have the same version of Git and the same version of Windows or it still just happens to work that way.

In particular, when you turn core.ignoreCase off (set it to false), Git believes that the OS can hold both uppercase and lowercase variants of some file. You would typically do this to manipulate Git's index. Running git add file.ext will now add the lowercase version and running git add FILE.EXT will add the uppercase version (and running git add FiLe.eXt will add a mixed-up-case third version). Running git rm --cached will let you remove any one particular case version from Git's index, all while believing that case is significant. Once you're done with this manipulation, you should restore the core.ignoreCase setting so that it describes how your system really works.

You can use git ls-files --stage (or even without --stage, --stage just makes the output much more detailed here) to see what files are in Git's index right now. Be aware that this command produces a full list of every file, which in a large repository, can be very long, so you might want to use bash-style redirection:

$ git ls-files --stage > output.tmp

and then use a file-viewer on output.tmp and then remove output.tmp when you're done, for instance.


1While macOS avoids some Windows issues, the case-folding thing is still in effect unless you make a case-sensitive file system. It's easy to make a mountable .dmg file that holds such a file system, and if you use a Mac, it's a good idea to have one around so that you can deal with Git repositories made on Linux.

Note that macOS has a rather different problem with certain Unicode sequences, which is as insurmountable in macOS as the aux.h problem is on Windows. So you might still want a Linux you can bring up on your Mac.

Related