KhueApps
Home/DevOps/Fix Git Case Collisions on Case-Insensitive Filesystems

Fix Git Case Collisions on Case-Insensitive Filesystems

Last updated: October 07, 2025

Overview

Case collisions happen when a repository contains two paths that differ only by letter case (e.g., README.md vs readme.md). On case-insensitive filesystems (Windows NTFS by default, macOS APFS/HFS+ default), these map to the same on-disk file, causing checkout, merge, or rename failures.

This guide shows how to detect, fix, and prevent these collisions in Git across Linux, macOS, and Windows.

Platform defaults:

  • Linux: case-sensitive (ext4, xfs)
  • macOS (default): case-insensitive but case-preserving (APFS/HFS+)
  • Windows (default): case-insensitive but case-preserving (NTFS)

Quickstart (most common fixes)

  • Case-only rename on case-insensitive FS:
    • git mv old tmp && git mv tmp Old && git commit -m "Case-only rename via hop"
  • Resolve existing collision in repo (created on Linux, breaks Windows/macOS):
    • On a case-sensitive machine, rename one file to a unique name, commit, push.
  • Detect collisions quickly:
    • git ls-files | awk '{k=tolower($0); a[k]=a[k]?a[k]"\n"$0:$0} END{for(k in a) if(index(a[k],"\n")) print a[k]}'
  • Avoid breaking Windows/macOS devs:
    • Enforce a no-collision rule with a pre-commit or CI check (script below).

Minimal Working Example

This demo simulates a collision on Linux and shows how to fix it so Windows/macOS can clone.

# 1) Create a repo with colliding paths (on Linux or a case-sensitive volume)
mkdir case-collide && cd case-collide
git init -q
printf 'hello\n' > readme.md
git add readme.md && git commit -q -m "add readme.md"
cp readme.md README.md
git add README.md && git commit -q -m "add README.md (collides on Windows/macOS)"

echo "-- Detect collisions (case-folded duplicates) --"
git ls-files | awk '{k=tolower($0); a[k]=a[k]?a[k]"\n"$0:$0} END{for(k in a) if(index(a[k],"\n")) print a[k]}'

# 2) Fix: disambiguate by renaming one of them
# Use a two-step rename if changing only case on a case-insensitive FS;
# here we choose a distinct name to be portable everywhere
git mv README.md README-UPPER.md
git commit -m "rename to avoid case collision"

# 3) Push; Windows/macOS can now clone/checkout safely

Expected result: The awk line prints both paths, confirming a collision. After the rename and commit, the collision disappears.

Common Scenarios and Fixes

1) Case-only rename fails locally

Symptoms on Windows/macOS: git mv foo.txt Foo.txt does nothing or results in odd status; or checkout creates/overwrites unexpectedly.

Fix (two-hop rename):

git mv foo.txt foo.tmp && git mv foo.tmp Foo.txt
git commit -m "Case-only rename via intermediate path"

Notes:

  • Use an intermediate name that does not currently exist.
  • git mv -f may still fail on some tools; the two-hop method is most reliable.

2) Checkout or merge fails due to collision from another platform

Symptoms:

  • The following untracked working tree files would be overwritten by checkout
  • unable to create file ...: File exists
  • updates were rejected because the working tree is not clean (after collision side-effects)

Fix (make the repo portable at the source):

  • On a case-sensitive system (Linux or a case-sensitive APFS volume), create a commit that disambiguates the paths:
# pick one to keep and rename the other
git mv README.md README-LEGACY.md
# or consolidate content, then remove the duplicate
git commit -m "Resolve case collision by renaming"
# push so others can fetch a portable tree
git push
  • After that, Windows/macOS developers can fetch and checkout without error.

3) Merge introduces a case-only rename conflict

Scenario: Branch A has README.md; Branch B renames it to Readme.md only by case.

Fix on a case-insensitive FS:

# On branch B, before merging:
git mv Readme.md Readme.tmp && git mv Readme.tmp README.md
git commit -m "Normalize filename before merge"
# Now merge B into A normally

Alternatively, perform the two-hop rename on the branch where you resolve the conflict, then complete the merge.

Detection and Prevention

One-off detection in a working tree

# List groups of files that collide when lowercased
git ls-files | awk '{k=tolower($0); a[k]=a[k]?a[k]"\n"$0:$0} END{for(k in a) if(index(a[k],"\n")) print "COLLISION:\n" a[k] "\n"}'

Pre-commit hook to block collisions

Place in .git/hooks/pre-commit and make executable:

#!/usr/bin/env bash
set -euo pipefail
mapfile -t files < <(git diff --cached --name-only)
awk_script='{
  k=tolower($0);
  if (seen[k] && seen[k]!=$0) { print "Case collision:", seen[k], "and", $0; exit 1 };
  seen[k]=$0
}'
printf '%s
' "${files[@]}" | awk "$awk_script"

This blocks any commit that introduces a case-duplicate among staged paths.

CI check (entire repo)

collisions=$(git ls-files | awk '{k=tolower($0); a[k]=a[k]?a[k]","$0:$0} END{for(k in a) if(index(a[k],",")) print a[k]}')
if [ -n "$collisions" ]; then
  echo "Case-colliding paths detected:"; echo "$collisions"; exit 1;
fi

core.ignorecase explained

  • On case-insensitive filesystems, Git auto-sets core.ignorecase=true. Leave it as-is.
  • For Linux repos that must stay portable to Windows/macOS, temporarily setting core.ignorecase=true can help reveal potential issues during testing, but do not commit with that assumption on a case-sensitive FS.
# Temporary local check (Linux):
git config core.ignorecase true
# run your collision detection and tests
# then revert
git config --unset core.ignorecase

History Cleanup (if collisions exist across history)

If historical commits contain collisions, some checkouts will always fail on Windows/macOS. Consider rewriting history to rename paths:

Using git filter-repo (recommended):

# Install per your platform, then:
git filter-repo --path-rename README.md:README-LEGACY.md

This rewrites all commits, renaming the path consistently. Force-push and coordinate with the team.

Fallback with git filter-branch (slower, legacy):

git filter-branch --index-filter \
  'git ls-files -s | sed "s#\tREADME.md#\tREADME-LEGACY.md#" | GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info && mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"' \
  -- --all

Platform Notes and Tips

  • Windows per-directory case sensitivity can be enabled, but mixed tooling may not honor it. Prefer repo-level fixes.
  • macOS case-sensitive volumes can be created, but teammates may not have them. Use them mainly to author fixes.
  • core.protectNTFS=true (default on Git for Windows) guards against other NTFS edge cases; keep it enabled.

Pitfalls

  • Do not toggle core.ignorecase globally on Windows/macOS; it won’t make the filesystem case-sensitive and can confuse Git.
  • A case-only rename in a single commit on case-insensitive FS may be ignored. Always use the two-hop rename.
  • Sparse checkout and .gitattributes do not solve collisions; they can only limit visibility or attributes, not path identity.
  • Clean up untracked files if they shadow paths during checkout: git clean -fd (review with -n first).

Performance Notes

  • Detecting collisions scales with the number of tracked files. Prefer streaming methods (git ls-files | awk) over invoking Git per file.
  • For large histories, git filter-repo is significantly faster and safer than filter-branch. Run it locally and push once, rather than many small rewrites.
  • Avoid repeated full-repo scans in pre-commit; only check staged paths for speed.

FAQ

  • Q: Can .gitattributes prevent case collisions?
    • A: No. Use hooks/CI to enforce and human-reviewed renames to resolve.
  • Q: Should I force core.ignorecase=false on Windows?
    • A: No. Leave the default true; use proper renames instead.
  • Q: Can I clone a repo with collisions on Windows?
    • A: Not safely. Ask a maintainer on a case-sensitive system to disambiguate and push a fix.
  • Q: Is there a one-command fix for case-only rename?
    • A: The reliable method is the two-hop rename: tmp name, then final name, commit.

Series: Git

DevOps