■はじめに
こんにちは。 株式会社iimonでエンジニアをしている遠藤です。
以前ファイル名の大文字/小文字を変更したかったので、mvコマンドで変更したのですが、差分が認識されないということがありました。 その後、git mvでファイル名を変更することで無事に差分を反映することができました。
そこで、今回はmvコマンドで差分が認識されなかった理由と、git mvの挙動について調べてみました。
■実施環境
- PC: MacBook Pro (2023) Apple M2 Pro
- OS: macOS 13.3
■実際の状況
実際にどういう状況だったのか再現します。
以下のように、ディレクトリ内にIndex.html
という名称のファイルがあります。
本来はindex.html
というファイル名にしたかったので、mvコマンドでIを小文字に修正します。
lsコマンドで確認すると、ファイル名はindex.html
に変更されています。
これをリモートリポジトリに反映したいですが、git statusで確認すると差分がありません。
■なぜmvコマンドで差分が出なかったのか
mvコマンドでファイル名の大文字/小文字を変更した時に差分が出なかった理由は、Gitの設定にありました。
ファイル名の大文字/小文字を区別するかはcore.ignoreCase
の値で設定します。
- true→ファイル名の大文字/小文字を区別しない
- false→ファイル名の大文字/小文字を区別する
実際に設定を確認すると、値はtrueになっていました。
そのため、mvコマンドでファイル名の大文字/小文字を修正しても差分が出なかったようです。
ということは、これをfalseに設定したら良いのかなと思うのですが、
Gitのドキュメントでcore.ignoreCase
についての記述を確認します。(DeepLで翻訳)
APFS、HFS+、FAT、NTFSなどのような大文字小文字を区別しないファイルシステムでGitがうまく動くようにするための様々な回避策を可能にする内部変数。例えば、Gitが "Makefile "を想定している時にディレクトリリストが "makefile "を見つけた場合、Gitはそれが本当に同じファイルであるとみなし、"Makefile "として記憶し続けます。デフォルトは false です。ただし、git-clone[1] や git-init[1] はリポジトリの作成時に core.ignoreCase を調べて適切なら true にします。Git は、オペレーティングシステムやファイルシステムに合わせてこの変数を適切に設定する必要があります。この値を変更すると、予期しない動作をすることがあります。
読む限り、不用意に値を変更せずにOSやファイルシステムに合わせた設定にしておくのが無難そうです。
ディスク情報を確認すると、ファイルシステムフォーマットはAPFS(暗号化)になっていました。これはファイル名の大文字/小文字を区別しないオプションなので、core.ignoreCase
の値はtrueのままにしておきます。
■git mvの挙動
git mvによるファイル名の変更は以下の形式で実行します。
git mv 現在のファイル名 新しいファイル名
詳しい使い方については、ドキュメントを参照してください。 https://git-scm.com/docs/git-mv
実際に先程のディレクトリ内で実行して、git statusで確認すると画像のように差分が出ています。
git mvはmvコマンドと同様にファイルやディレクトリの名前変更・移動をする際に使うコマンドですが、実行時に次のGit操作が行われる点が異なります。
- インデックス*1から古いパス名を取り除く
- インデックスに新しいパス名を追加する
- オブジェクトストアのコンテンツをそのまま残し、新しいパスにそのコンテンツを結びつける
今回の例で実際にインデックスの変化を確認してみます。
先ほどのgit mvによる変更を元の状態に戻して、ファイル名を修正する前にインデックスを確認します。
Index.htmlというパス名と、コンテンツから生成されるハッシュ値が結びついています。
再びgit mvでファイル名を修正し、インデックスを確認します。
古いパス名(Index.html)が取り除かれ、新しいパス名(index.html)が追加されています。
結びついているハッシュ値は同じです。
ちなみに、変更をコミットして、--followオプションを指定してログを確認すると、ファイル名変更前のログを含む完全な履歴を取得できます。
Gitは名称変更を追跡せず、コンテンツを追跡するため、削除されたファイルと追加されたファイルのコンテンツの内容(ハッシュ値)をマッチングさせ、その類似度でリネームとして判定しているようです。そのため、削除されたファイルと新しく作成されたファイルで全く同じハッシュ値が結びついていればリネームとして判定されます。
前述したgit mvの挙動は、通常はmvコマンドを使う場合だと次のコマンドを実行すると同じGit操作になります。
mv 現在のファイル名 新しいファイル名 git rm 現在のファイル名 git add 新しいファイル名
ただ今回の場合は、core.ignoreCase
の値がtrueに設定されているため、git rmでIndex.htmlを指定すると、名称をindex.htmlに修正したファイルが消えてしまうので、
次の手順を実行するとgit mvを使うのと同様にリネームすることができました。(あくまでgit mvの挙動を再現するための一例です)
git rm でインデックスから現在のパス名を取り除く
git rm Index.html
実行後のインデックスを見ると取り除かれていることがわかります。
touchコマンドで新しいファイルを生成
touch index.html
古いファイルの中身を新しいファイルに写す
git addで新しいパス名をインデックスに追加
git add index.html
インデックスを見ると新しいパス名が追加され、コンテンツが全く同じであるため同じハッシュ値が結びついています。 そのため、git statusを確認するとrenameとして差分が認識されています。
■まとめ
簡単にまとめるとgit mvは、
インデックスから古いパス名を削除
インデックスに新しいパス名を追加
新しいパス名に古いパス名と同じハッシュ値を結びつける
という操作を行なっていました。 そのため、同様の操作を再現すればgit mvを使わずにリネームすることも可能ということもわかりました。(使いたくない理由が特になければgit mvを使うのが早いと思いますが)
git mv実行時の挙動について調べることでGitの内部動作への理解も深まり面白かったです。 今後もしっかりGitについて理解を深めていこうと思います。
また、弊社ではエンジニアを募集しております。
ぜひカジュアル面談でお話ししましょう!
ご興味ありましたら、ご応募ください! Wantedly / Green
参考資料
PremKumarPonuthorai, JonLoeliger著, 萬谷暢崇監訳, 長尾高弘訳(2024)『実用 Git 第3版』オライリー・ジャパン.