What happened: a deploy failed with no code changes

I was rewriting my blog from Nuxt.js to Next.js on macOS Catalina (10.15), running Git 2.24.3. During development I renamed a React component file from Logo.js to logo.js to match the project’s naming convention. The file looked correct in Finder, the import worked locally, and git status showed nothing.

Then CI failed:

Module not found: Can't resolve './logo'
  in '/app/src/components'

The build ran on a Linux container (Ubuntu 20.04, ext4). I was working alone on this project. No merges, no rebase, no history rewriting. Just a simple filename case change that Git silently ignored.

Why does Git ignore case changes on macOS?

When you run git init on macOS, Git probes the filesystem and sets a config value:

git config --get core.ignorecase
# true

macOS uses APFS (or HFS+ on older systems), both of which are case-insensitive but case-preserving by default. That means the filesystem treats Logo.js and logo.js as the same file. Git respects this by setting core.ignorecase = true, which tells it to skip case-only differences when comparing filenames in the working tree against the index.

Here is what that looks like in practice:

# macOS (APFS, case-insensitive)
$ git --version
git version 2.24.3 (Apple Git-128)

$ touch Logo.js
$ git add Logo.js && git commit -m "add Logo.js"

$ mv Logo.js logo.js
$ git status
On branch main
nothing to commit, working tree clean

Git sees no change. The filesystem reports the same inode, and core.ignorecase = true tells Git not to compare case. The rename exists on disk but never makes it into the index.

On Linux (ext4, case-sensitive), mv Logo.js logo.js creates a genuinely different filename. Git sees it immediately:

# Linux (ext4, case-sensitive)
$ mv Logo.js logo.js
$ git status
Changes not staged for commit:
  renamed:    Logo.js -> logo.js

This is the root cause: your local working tree and the Git index disagree about the filename, but Git is configured to ignore the disagreement.

How to reproduce this in 60 seconds

Run this on any macOS machine with default filesystem settings:

mkdir /tmp/git-case-test && cd /tmp/git-case-test
git init
echo "hello" > Something.txt
git add Something.txt
git commit -m "initial"

# Rename with case change only
mv Something.txt something.txt

# Git sees nothing
git status
# On branch main
# nothing to commit, working tree clean

# Prove the file actually changed on disk
ls -la
# -rw-r--r--  1 user  staff  6 Oct  2 10:00 something.txt

The file is something.txt on disk, but Git’s index still records Something.txt. Anyone who clones this repo on Linux gets the old name.

How a case-insensitive filesystem broke our deploy

In my case, the Next.js dev server on macOS resolved import Logo from './logo' successfully because the filesystem matched Logo.js to the ./logo import (case-insensitive lookup). On the Linux CI server, the filesystem is case-sensitive, so ./logo only matches logo.js exactly. Since Git had never recorded the rename, the CI clone still contained Logo.js, and the import failed.

The timeline:

  1. I renamed Logo.js to logo.js locally using mv.
  2. git status showed nothing. I assumed the rename was tracked.
  3. I updated the import to ./logo and committed.
  4. Locally, everything worked (macOS resolved both cases).
  5. CI cloned the repo on Linux. The file was still Logo.js. The import ./logo failed.

The fix: use git mv

The correct way to rename a file with only a case change on macOS:

git mv Logo.js logo.js

git mv updates the index directly, bypassing the filesystem’s case-insensitivity. Git records the rename as a proper change:

$ git mv Logo.js logo.js
$ git status
On branch main
Changes to be committed:
  renamed:    Logo.js -> logo.js

If git mv complains that the destination already exists (which can happen on case-insensitive filesystems), use a two-step rename:

git mv Logo.js logo-tmp.js
git mv logo-tmp.js logo.js

Why we added a CI check at USEO

At USEO, we hit this exact class of bug during a Rails project. A developer renamed a controller from UsersController to Users_controller (matching Rails naming conventions) using their IDE on macOS. The rename looked correct locally, but on the Linux production server, Rails autoloading could not find the file because the old filename was still in Git’s index.

After that incident, we added a simple CI step that catches case-only filename conflicts:

# Detect files that differ only by case in the Git index
git ls-files | sort -f | uniq -di

If this command produces any output, the build fails. It takes under a second to run and has saved us from this problem multiple times since.

Can you set core.ignorecase to false on macOS?

Technically yes:

git config core.ignorecase false

But this is not recommended on a case-insensitive filesystem. Git will start reporting phantom changes, treat Logo.js and logo.js as two separate files (even though the filesystem cannot hold both), and produce confusing merge conflicts. The Git documentation explicitly warns against changing this value manually.

The better approach: always use git mv for case-only renames, and add automated checks in CI.

Key takeaways

  1. macOS filesystems are case-insensitive by default. Git adapts by setting core.ignorecase = true, which means case-only renames are invisible to git status.
  2. Use git mv for case-only renames. It updates the index directly and records the change.
  3. Add a CI check for case conflicts. git ls-files | sort -f | uniq -di catches problems before they reach production.
  4. Never assume local success means CI success. Filesystem behavior differences between macOS and Linux are a real and recurring source of deploy failures.