The choice
APFS on macOS is case-insensitive by default. I guess this is done to keep traditions, since HFS+ (its predecessor) was case-insensitive by default too; and I actually think it is, in general, a good idea. Most persons, technical or not, would usually regard a document titled Financial results the same as Financial Results in the real world; so Apple translated that expectation to the computer world.
The implementation is done like this: when you write a file for the first time, it will actually store the path to that file in the specific casing that you wrote it in, and will return that casing when you e.g. list the containing folder. But, when a program is trying to locate this file, APFS will happily accept any casing you throw at it, and still return the correct file.
$ touch TestFile
$ echo "Hello" > testfile
$ cat tEsTfIlE
Hello
$ ls
TestFile
# Even renames work!
$ mv TestFile testfile
$ ls
testfileThis behavior is called case-preserving. And again, it generally follows the behaviors that one has come to expect from real-world handling text. Even when we regard Financial results the same as Financial Results, we'd not be happy if the letters on the paper changed to financial results behind our backs. We want the medium to preserve and communicate the choices we made.
The limitation
That's all fine and dandy then, no? Well, no, there's one case where it might break down a bit:
$ touch TestFile
$ mv testfile testfile...what should happen now? Our intuition fails a bit here because this is non-sensical in the real world; but this is valid in the Terminal, it doesn't fail. I'd personally hope that the file would be located and the new name recorded, so that it is canonically lowercase.
Let's see what actually happens:
$ touch TestFile
$ mv testfile testfile
$ ls
TestFileDamn. Just nothing. It seems the implementation here is breaking down a bit: we're asking to rename a file to itself, so of course nothing happens. But that's just because APFS decided to make them equal!
The problem
This quirk can become a genuine headache when working with tools that have their own concepts of files and paths. For example, Git.
Git copies file paths and contents into an internal database (its commits and trees). And since Git can work in both case-sensitive and case-insensitive systems (and case-sensitive strings can be converted into case-insensitive ones, but not the other way around), its internal representation needs to be case-sensitive too.
This can create a mismatch between what Git thinks exists and what your filesystem actually allows. While Git has some protections, it's not entirely difficult to get yourself in this scenario if you git checkout a lot:
$ ls
testfile
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: TestFile
$ git add TestFile
$ git commit -m "Rename"
[main 20d8b9b] Rename
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 TestFile
$ git ls-files
TestFile
testfile Not good. And it all gets worse if you collaborate with people on case-sensitive systems, as an entirely sensible change on their machine (e.g. docs/{testDocs.md => TestDocs.md} now has become a file that mysteriously always shows as changed on your system, while you can't find the reason.
Yes, I wrote this blog post because of that. It happens infrequently enough that when it happens I forget this is a thing, and I get too incensed when the penny finally drops.
The conclusion
What can I do? Nothing. This is a result of entirely sensible choices on everyone's part, and the result of no solution design surviving contact with reality. Just remember what to do if you need to reset casing in a macOS system:
$ mv testfile testfile-tmp
$ mv testfile-tmp TestFile