タイトル通りです。丸一日消滅したので同じことが起きないように書き残しておきます。
要約: 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
を使うことは二度とないでしょう。
-
git gc --aggressive --prune=now
やgit reflog expire --expire=now --all
、git -c gc.reflogExpire=0 -c gc.reflogExpireUnreachable=0 -c gc.rerereresolved=0 -c gc.rerereunresolved=0 -c gc.pruneExpire=now gc
、git repack -Adf
など ↩︎ -
git_find_big.sh
では単体のファイルサイズしか確認できないため、小さなゴミファイルが大量に存在する大きなディレクトリを発見することができません ↩︎ -
--ref
オプションを利用して特定のブランチからのみパスを削除することもできます ↩︎ - もしかしたら環境依存かもしれません。自分は Windows 10 で WSL を通さずに利用しています。 ↩︎