問題の概要
utils.js を Utils.js にリネームした — 大文字1文字の違い。git status を実行しても何も表示されない。まるで何も起きていなかったかのようにGitは振る舞う。
ローカルでは問題なく動作する。しかしLinuxのCIサーバーにプッシュするとビルドが壊れる — サーバーは Utils.js を探しているのに、リポジトリにはまだ utils.js が残っているためインポートが失敗する。ローカルでは潜伏し、本番環境で牙をむく典型的なバグだ。
なぜこうなるのか
Windows(NTFS)とmacOS(デフォルト設定のHFS+/APFS)は大文字・小文字を区別しないファイルシステムを使用している。OSにとって file.txt と File.txt は同じファイルだ。Gitはファイルシステムに依存して変更を検出するため、違いを認識できない。
さらに関係するGitの設定オプションがある: core.ignoreCase。WindowsとmacOSでは、インストール時にGitがこれを自動的に true に設定する。これは意図的な動作で、大文字・小文字を区別しないシステムでの誤検知を防ぐためだ。しかしその副作用として、Gitは実際の大文字・小文字のリネームを無視してしまう。
Linuxは異なる。ファイルシステムが大文字・小文字を区別するため、file.txt と File.txt は別々のファイルとして扱われる。コードがLinux上で動作する場合 — サーバー、Dockerコンテナ、GitHub Actions — リポジトリに記録されたファイル名を正確に参照する。Gitがリネームを追跡していなければ古いケーシングのままになり、参照が壊れる。
修正方法1: 一時的な名前を使った git mv(推奨)
一時的な中間名を経由してリネームする。これによりGitは1つの見えないリネームではなく、2つの別々のリネームとして記録するよう誘導される。
# ステップ1: 一時的な名前にリネーム
git mv utils.js utils_temp.js
# ステップ2: 一時名から最終的な名前にリネーム
git mv utils_temp.js Utils.js
# ステップ3: Gitが追跡していることを確認
git status
ステージングエリアには以下のように表示されるはずだ:
Changes to be committed:
renamed: utils.js -> Utils.js
# ステップ4: コミット
git commit -m "Rename utils.js to Utils.js"
修正方法2: core.ignoreCase = false に設定して強制リネーム
別の方法として、この操作だけのためにGitを大文字・小文字を区別するモードに切り替える:
# このリポジトリでGitを大文字・小文字を区別するように設定
git config core.ignoreCase false
# 直接リネームを実行
git mv utils.js Utils.js
git status
コミット後、core.ignoreCase false のままにするか元に戻すかは任意だ。注意点として: 大文字・小文字を区別しないファイルシステムで false のままにすると、変更していないファイルに対してGitが幽霊のような差分を報告することがある。修正方法1ではその問題が完全に回避できる。
# オプション: コミット後に設定を元に戻す
git config core.ignoreCase true
修正方法3: すでに間違ったケーシングでコミットしてしまった場合
ファイルがすでに間違ったケーシングでコミット・プッシュされている場合、インデックスを直接更新する必要がある:
# Gitのインデックスから古いファイルを削除(ディスクからは削除しない)
git rm --cached utils.js
# 正しい新しい名前でファイルを追加
git add Utils.js
# 修正をコミット
git commit -m "Fix: rename utils.js to Utils.js (case correction)"
ファイルが存在しないとGitが文句を言う場合は、最終手段として全てを再インデックスする:
# macOS/Windowsでファイルに変更が表示されない場合:
git rm -r --cached .
git add .
git commit -m "Fix file casing in Git index"
警告: git rm -r --cached . は全てのステージングを解除して再追加する。実際のファイルは変更されない — Gitのインデックスにのみ影響する。それでも、含めたくない無関係なファイルを見落とさないよう、コミット前に git status を実行して確認すること。
確認: 修正が正しく適用されたか検証する
最後のコミットにリネームが正しく記録されているか確認する:
# 最新のコミットにリネームが表示されているか確認
git log --oneline --name-status -1
期待される出力:
a3f9c12 Rename utils.js to Utils.js
R100 utils.js Utils.js
R100 はGitが100%の類似度でリネームを記録したことを意味する。D(削除)と A(追加)が表示される場合は理想的ではないが、それでも機能する — 新規クローン後にLinux上で正しい名前のファイルが存在することになる。
プッシュしてリモートで確認する:
git push origin main
# その後GitHub/GitLabで確認 — ファイルが新しいケーシングで表示されているはずだ
予防策: 大文字・小文字リネーム用のGitエイリアスを追加する
定期的に大文字・小文字のリネームを行う場合は、このエイリアスをグローバル設定に一度追加すれば、二段階の手順を気にする必要がなくなる:
git config --global alias.mvcased '!f() { git mv "$1" "${1}_tmp" && git mv "${1}_tmp" "$2"; }; f'
使用方法:
git mvcased utils.js Utils.js
クイックリファレンス
- シナリオ1 — まだコミットしていないファイル:
git mv file.txt temp.txt && git mv temp.txt File.txt - シナリオ2 — コミット済み、未プッシュ:
core.ignoreCase falseに設定し、git mvを使用後、設定を元に戻す - シナリオ3 — すでにプッシュ済み:
git rm --cached oldname+git add newname+ コミット + プッシュ - シナリオ4 — 複数ファイルが影響を受けている:
git rm -r --cached . && git add .で全てを再インデックス

