git でリポジトリを整理して容量削減するときに嵌った話

タイトル通りです。丸一日消滅したので同じことが起きないように書き残しておきます。

要約: filter-branch を使うのはやめよう。filter-repo を使おう。

背景

個人の進捗管理用に使用しているリポジトリの容量が肥大化してきており、過去にコミットしてしまった大きなファイル(オセロの自己対戦ログとか学習済みパラメータとか!)や不要なファイル(実行可能ファイル、トランスパイルされた巨大なソースコードなど)を歴史から抹消してリポジトリをスマートにしようということになりました。

調べてみるとgit_find_big.sh をダウンロードしてきて大きなファイルを見つけ、git filter-branch コマンドで各コミットに対し git rm 等のコマンドを適用して歴史から消すという方法が多数ヒットします。自分もこれに従いました。

問題

とにかく遅い

まず git filter-branch非常に遅いです。自分のリポジトリは 1.5 GB ほどでコミット数が 300 程度だったのですが、全コミットを遡って修正を適用するのに数分~10分程度かかりました。

そして、その後クリーンアップのために実行する --aggressive 付きの git gc恐ろしく遅いです。90% あたりから指数的に遅くなり、大量のメモリと時間を消費します。

ただ、まあそもそもリポジトリの容量削減自体滅多にすることのない作業なので、時間がかかるのも仕方ないかと思い終わるのを待っていました。

消えてない

そしてようやく全ての作業が終わり、git log から歴史を確認して該当のファイル群が消えているのを確認、reflog 等も削除して一件落着……と思いきや、リポジトリの容量(.git ディレクトリのサイズ)が全く減っていないことに気が付きます。

念のためもう一度 git_find_big.sh を実行してみると、歴史から抹消したはずのファイル群がそのままの容量で発見されました。どうやら、「歴史からは正しく消滅したが、git の管理下にはファイルが残っている」状態になってしまったようです。

一回目は運悪く失敗したのかな程度に思っていましたが、残念なことに何度繰り返しても結果は同じでした。散々ググったり友人に聞いたりして様々なコマンドを試しました[1]が、結局解決することはありませんでした。こうして一日が消滅します。

git-filter-repo を試す

コマンドで git filter-branch を実行すると 「サードパーティ製の filter-repo とかを使った方がいいよ」と言われます。その通りでした。ただしデフォルトでは入っていないので、環境に応じて自分でインストールする必要があります。

強力な分析機能

git-filter-repo には --analyze オプションがあり、リポジトリのサイズに関する様々な情報を分析してくれます。

git filter-repo --analyze

分析結果は .git/filter-repo/analysis/ 以下に保存されます。

  • 全部のファイル path-all-sizes.txt
  • 過去に削除されたファイル path-deleted-sizes.txt
  • 全部のディレクトリ directories-all-sizes.txt
  • 過去に削除されたディレクトリ directories-deleted-sizes.txt

などを全て内部的な圧縮後のサイズの降順にまとめてくれ、どのファイルやディレクトリを抹消するかの参考にすることができます。特にディレクトリをサイズ順に並べてくれるのは非常に嬉しいです[2]

シンプルで使いやすい

削除したいファイルやディレクトリを集めたら、それぞれを --path オプションで連結して filter-repo に渡します。ブランチ指定は(デフォルトで全部のブランチから削除するので)不要です[3]

例:hoge/fuga/, hoge/foo.txt を削除したい場合

git filter-repo --path hoge/fuga/ --path hoge/foo.txt --invert-path

パスはデフォルトでホワイトリスト制になっているので、最後の --invert-path を忘れると指定したパス以外が全部消えます(これいる?)。

また、--path の代わりに --path-glob を使うことでワイルドカードを使ったり --path-regex を使うことで正規表現を利用してパスを指定したりすることができます。

ただし、--path-glob を使う際に hoge/*.txt のような文字列を渡すと hoge 以下の .txt ファイルがサブディレクトリを含めて全部削除される[4]ので注意してください。hoge 直下の .txt ファイルだけを削除したい場合は、--path-regex を使って ^hoge/[^/]+\.txt$ のように指定してやるとうまくいくようです。

詳しい説明はユーザーマニュアルを参照してください。特に、事故を防ぐため clone したてのリポジトリで実行する点に注意してください。「リモートのブランチがないよ~」って人は .git ディレクトリだけコピーしてバックアップを取っておけば安心と思います。

超速い

filter-branch を使っていた頃からすると信じられないくらい速いです。あまりにすぐ終わるのでエラーで止まったかと思いましたが、きちんと正常終了していました。

さらに、filter-branch では必要だった git gc 等による後始末もまとめて実行してくれるため、更に時間を取られることがありません。

そして何より、どうやっても減らなかった .git ディレクトリのサイズが小さくなり、消したファイル群は git_find_big.sh に検出されなくなっています。

実行後

作者としては force push するよりも別のリポジトリに push してほしいらしく、実行後に remote origin の情報が消滅します。force push する場合は自分で再度 remote origin の設定を行ってから push する必要があります。

また、書き変わったコミットの旧ハッシュを ref を用いて保存してくれているようで、ハッシュが書き変わった後も引き続き旧ハッシュでのコミット検索ができるようになっています。自分としてはこの機能は不要だったため、.git/packed-refs に大量に追加された ref を削除してしまいました。

まとめ

filter-branch に散々苦しめられた後 filter-repo を使ったら、あっけないほど簡単に終わりました。もう filter-branch を使うことは二度とないでしょう。


  1. git gc --aggressive --prune=nowgit reflog expire --expire=now --allgit -c gc.reflogExpire=0 -c gc.reflogExpireUnreachable=0 -c gc.rerereresolved=0 -c gc.rerereunresolved=0 -c gc.pruneExpire=now gcgit repack -Adf など ↩︎
  2. git_find_big.sh では単体のファイルサイズしか確認できないため、小さなゴミファイルが大量に存在する大きなディレクトリを発見することができません ↩︎
  3. --ref オプションを利用して特定のブランチからのみパスを削除することもできます ↩︎
  4. もしかしたら環境依存かもしれません。自分は Windows 10 で WSL を通さずに利用しています。 ↩︎

このエントリーをはてなブックマークに追加