リポジトリが異常に重い原因を調べてみた
はじめに
AnsibleのGitリポジトリで、git clone に異常な時間がかかるものがあった。
調べてみると、インストーラ類のバイナリが大量にコミットされており、リポジトリ全体が肥大化していた。
原因をGitの内部構造から掘り下げ、「なぜこんなに重くなってしまったのか」を整理してみた。
ざっくりGitの仕組み
Gitはファイルを次の3種類のオブジェクトで管理している。
| オブジェクト | 内容 | 役割 |
|---|---|---|
| blob | ファイルの中身そのもの | 実際のデータ本体(テキストや画像など) |
| tree | ディレクトリ構造 | blobやサブディレクトリをまとめる |
| commit | スナップショット情報 | treeへのポインタ+メッセージ+メタデータ |
ファイルをコミットすると、Gitはその内容をblob(バイナリオブジェクト)に変換し、
SHA-1ハッシュで識別して保存する。つまり「ファイルの中身」をキーにして履歴を構築している。
blobのイメージ
file1.txt → blob(a1b2c3d)
file2.txt → blob(e4f5g6h)
同じ内容のファイルなら同じblobが使い回されるが、
内容が1文字でも変われば新しいblobが作られる。
Commitのイメージ
[Commit A] ──┐
│
▼
[Tree A]
├── file1.txt → blob(aaa111)
└── file2.txt → blob(bbb222)
[Commit B] ──┐
│
▼
[Tree B]
├── file1.txt → blob(ccc333) ← (内容変更)
└── file2.txt → blob(bbb222)
このように、変更されたファイルだけ新しいblobが作られ、
変更されていないファイルは同じblobを再利用する。
ここまでがGitの“中身の基本構造”だ。
バイナリをGitで管理するとどうなるか
バイナリ(例:zip、tar.gz、exe など)をGitにコミットすると、バージョンごとにファイル全体のコピーが履歴として残る。
たとえ小さな修正でも、Gitはファイルの「全体」を新しいオブジェクトとして保持するため、履歴が積み重なるほどリポジトリがどんどん太っていく。
実際に起きるデメリット
- リポジトリサイズの肥大化(cloneやfetchが遅くなる)
- バイナリは差分表示ができず、レビュー時の比較も不可能
- Gitの利点(履歴管理・差分追跡)がほとんど活かせない
このため、バイナリはGitではなくPackage RegistryやArtifact Repositoryに分離するのが一般的なベストプラクティスだ。
Git標準コマンドで容量を可視化する
Git自体に履歴内の大容量ファイルを調査する機能はないが、内部構造をたどれば確認できる。
git rev-list --objects --all \
| git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
| awk '$1=="blob"{print $3"\t"$4}' \
| sort -nr | head -20
出力例
53498273 bigfile.zip
21249382 dataset.tar.gz
10512342 images/large_background.png
...
| コマンド部分 | 役割 |
|---|---|
git rev-list --objects --all |
すべてのコミットに含まれるオブジェクト一覧を出力 |
git cat-file --batch-check |
各オブジェクトの型とサイズを取得 |
awk '$1=="blob"' |
ファイル(blob)のみ抽出 |
sort -nr |
サイズの大きい順にソート |
head -20 |
上位20件を表示 |
この方法は追加ツール不要・Git標準コマンドだけで完結するのがポイント。
履歴を巻き戻したり危険な書き換えを行わず、現状のリポジトリ状態を安全に可視化できる。
まとめ
Gitは便利な履歴管理ツールだが、**「何を入れるか」**を誤ると簡単にリポジトリが肥大化する。
特にバイナリを頻繁にコミットしていると、後から履歴を軽くするのはかなり大変だ。
今回の調査を通して、改めて「コードと成果物は分けるべき」という原則を実感した。
次に取るべき対応 (別途整理予定)
リポジトリを軽くするには、主に次の2つのアプローチが必要になりそうだ。
-
今後の対策:
filterの導入
git lfsやgit filter-repoを利用して、バイナリをGitの管理対象から外す仕組みを整える。
例:git filter-repo --path-glob '*.zip' --invert-paths特定拡張子を履歴から安全に除外できる。
-
過去の履歴の整理:不要ファイルの削除
すでにコミット済みの大容量バイナリは、
git filter-repoやBFG Repo-Cleanerを使って履歴から削除する必要がある。
削除後はgit gc --aggressive --prune=nowを実行し、再圧縮で実際の容量を削減する。
Discussion