Gitに大容量ファイルをコミットしたとき — 履歴整理とLFSへの移行

読了 6分

動画やデザインの元データをコミットして、pushが拒否されることがあります。GitHubは100MBを超えるファイルのpushをブロックするためです。もっと静かに進行する事故もあります。ファイル1つずつは制限に掛からないのに、バイナリがバージョンを重ねて積み上がり、ある日リポジトリが数GBに膨らんでcloneに10分もかかる状況です。どちらも原因は同じです。一度コミットされたファイルは、消しても履歴の中にすべてのバージョンが残るというGitの保存モデルです。この記事では、問題のファイルを見つける診断から履歴の整理、そして引き続き必要なファイルをLFSへ移す手順までを整理します。

診断 — リポジトリで最大のファイルを見つける #

まず、リポジトリが実際にどれだけ重いのかを確認します。

リポジトリ容量の確認
git count-objects -vH
# count: 1324
# size-pack: 2.31 GiB
# ...

size-packが履歴全体の占める実際のサイズです。作業ディレクトリに見えるファイルサイズと関係なくこの値が大きいなら、履歴のどこかに重いblobがあります。原因はパイプライン1本で見つけられます。

最大のblob上位10個
git rev-list --objects --all |
  git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' |
  awk '/^blob/ {print $3, $4}' |
  sort -rn |
  head -10
# 734003200 assets/demo-video.mp4
# 209715200 design/main.psd
# ...

履歴全体のすべてのオブジェクトをサイズ順に並べた結果です。ここに出たパスが整理対象のリストになります。すでに削除されたファイルでも、履歴に残っていればこのリストに現れます。

コミットした直後なら — push前のローカル整理 #

まだpushしていなければ、問題は自分のコンピュータの中にあり、整理も簡単です。最後のコミットに入ったのなら、追跡だけ解除してコミットを置き換えます。

最後のコミットから除去
git rm --cached assets/demo-video.mp4   # 追跡を解除し、ファイル自体は残る
echo "*.mp4" >> .gitignore
git add .gitignore
git commit --amend --no-edit

数コミット前に入ったのなら、interactive rebaseで該当コミットに止まって同じ作業をします。

以前のコミットから除去
git rebase -i <該当コミットの直前のコミット>
# todoリストで該当コミットを edit に変更

git rm --cached assets/demo-video.mp4
git commit --amend --no-edit
git rebase --continue

push前に捕まえたならここで終わりです。下の協力の手続きはいっさい必要ないので、push直前にgit diff --staged --statで大容量ファイルが混ざっていないか確認する習慣の価値は大きいです。

履歴の奥深くにあるなら — filter-repo #

すでにpushされ、複数のコミットにわたって積み上がっているなら、履歴全体を書き換える作業が必要です。ツールはgit filter-repoを使います。filter-repoは新しくcloneしたリポジトリで実行するのが前提なので、整理専用のクローンから取得します。

サイズ基準で一括除去
git clone https://github.com/example/my-service.git cleanup
cd cleanup
git filter-repo --strip-blobs-bigger-than 50M

--strip-blobs-bigger-thanは、指定サイズを超えるすべてのblobを履歴全体から除去します。特定のファイルやディレクトリだけを除去するなら、パス基準を使います。

パス基準で除去
git filter-repo --invert-paths --path assets/demo-video.mp4
git filter-repo --invert-paths --path design/raw/

整理された履歴は、すべてのコミットハッシュが変わった新しい履歴なので、リモートをつなぎ直して強制的に上げ、チーム全員にre-cloneを告知しなければなりません。古いクローンからpushすると、消したblobがよみがえります。この協力の手続きは秘密鍵の事故と完全に同じで、手順の詳細と注意点はGitに秘密鍵をコミットしたとき — キーのローテーションとfilter-repoでの履歴整理で扱いました。

整理した履歴を上げる
git remote add origin https://github.com/example/my-service.git
git push --force --all
git push --force --tags
注記
GitHubのリポジトリ容量表示は、履歴整理の後もすぐには減らないことがあります。サーバー側のガベージコレクションが動くのに時間がかかるためです。force push直後の容量の数字だけを見て、整理が失敗したと判断しなくて大丈夫です。

整理の後 — 引き続き必要なファイルはLFSへ #

除去で終わるファイルもあれば、これからもバージョン管理が必要なファイルもあります。デザインの元データや学習済みモデルのように扱い続ける大容量アセットなら、Git LFSへ移します。LFSはリポジトリに小さなポインタファイルだけを残し、実際の内容は別のストレージに保存する拡張で、概要はGit実務ワークフロー #7 モノレポとGit — sparse-checkout・サブモジュール・LFSで扱いました。

すでに履歴に入ったファイルをLFSへ変える専用コマンドがあります。

履歴のファイルをLFSへ移行
git lfs migrate import --include="*.psd" --everything

migrate importは、指定パターンのファイルを履歴全体(--everythingはすべてのブランチ対象)でLFSポインタに変えてくれます。これも履歴の書き換えなのでコミットハッシュが変わり、force pushとre-clone告知という同じ協力の手続きが付いてくる点は変わりません。どうせfilter-repoで履歴を書き換えるところなら、LFSへの移行まで1回の書き換えサイクルにまとめて処理するのが、チームを二度煩わせない方法です。

移行後は、新しいファイルが自然にLFSへ入るよう追跡ルールをコミットしておきます。

以後のコミットのための追跡ルール
git lfs track "*.psd"
git add .gitattributes
git commit -m "Track PSD files with LFS"

予防 #

  • 基準を知っておきます。GitHubは50MBから警告を出し、100MBからpushをブロックします。警告が見えたなら、すでに対処すべき時点です。
  • 最初からLFSで始めます。大容量アセットが見込まれるプロジェクトなら、最初のコミットの前にgit lfs trackのルールから整えておきます。履歴に入った後で移すコストとは比べものになりません。
  • 生成物は.gitignoreで防ぎます。ビルド成果物、圧縮アーカイブ、レンダリング結果のように作り直せるファイルは、そもそもバージョン管理の対象ではありません。
ヒント
LFSを使うと決めたら、ホスティングの料金もあわせて確認します。GitHubのLFSストレージと帯域の無料枠は小さめなので、アセットの多いチームは追加の料金プランや別ストレージ連携の検討をおすすめします。

まとめ #

対応の順序を整理します。

  1. 診断git count-objects -vHとblobサイズのパイプラインで問題のファイルを特定します。
  2. 整理 — push前ならamendやrebaseで、push後ならfresh cloneでfilter-repoを使って履歴を書き換えます。
  3. 告知 — force push後、チーム全員がre-cloneするよう案内します。
  4. 再配置 — 引き続き必要なアセットはgit lfs migrate importでLFSへ移し、追跡ルールをコミットします。

大容量ファイルの事故は、秘密鍵の事故と違って時間との勝負ではありません。その代わり放置するほど履歴にバージョンが積み上がって整理の範囲が広がるので、50MBの警告が見えた時点ですぐ処理するのが最も安上がりな対応です。

X