📚

リポジトリが異常に重い原因を調べてみた

に公開

はじめに

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つのアプローチが必要になりそうだ。

  1. 今後の対策:filter の導入
    git lfsgit filter-repo を利用して、バイナリをGitの管理対象から外す仕組みを整える。
    例:

    git filter-repo --path-glob '*.zip' --invert-paths
    

    特定拡張子を履歴から安全に除外できる。

  2. 過去の履歴の整理:不要ファイルの削除
    すでにコミット済みの大容量バイナリは、
    git filter-repoBFG Repo-Cleaner を使って履歴から削除する必要がある。
    削除後は

    git gc --aggressive --prune=now
    

    を実行し、再圧縮で実際の容量を削減する。

参考文献

Discussion