Claude Codeで巨大ファイルを履歴から除去 — git filter-repoの手順
リポジトリが重い原因の巨大blobを特定し、ミラークローンでgit filter-repoにより履歴から除去して、force pushと再cloneまで進める手順です。Claude Codeに任せる範囲も扱います。
リポジトリが重い原因が過去に紛れ込んだ巨大ファイルなら、ファイルを今のコミットで消しても軽くなりません。履歴に残るblobがcloneのたびに運ばれるからです。git filter-repoで履歴ごと書き換えれば小さくできますが、書き換えは元に戻せず、関係者全員のクローンにも影響します。この記事は、サイズの特定、ミラークローンでの除去、force pushと再cloneの段取りまでを順に並べます。
秘密情報の混入が理由で履歴を消す場合は、値の失効が先になります。その順序はClaude Codeでシークレットをコミットした後の対応手順にあります。ここで扱うのは、サイズだけが問題の場合です。
巨大ファイルを履歴から外す全体の流れ
- 1
1. 重さの正体を測る
git count-objectsとgit cat-fileで、サイズ上位のblobとパスを洗い出します。 - 2
2. ミラークローンを作る
元のリポジトリには触れず、作業専用のクローンで書き換えます。
- 3
3. git filter-repoで除去する
サイズの閾値、パス、blobのIDのいずれかで対象を指定します。
- 4
4. 小さくなったか検証する
--analyzeとサイズの再計測で、狙ったものだけが消えたかを見ます。 - 5
5. force pushして再cloneを依頼する
全てのrefを押し返し、関係者には新しくcloneし直してもらいます。
どのファイルが重さの原因かを測る
まず全体の大きさを見ます。git count-objects -vHは、packファイルが占める容量をsize-packとして、緩いオブジェクトの容量をsizeとして人が読める単位で出します。in-packはpackに入ったオブジェクトの数、garbageはオブジェクトでもpackでもないファイルの数です。cloneで運ばれる量の目安はこのsize-packで、作業ツリーの大きさとは別物です。
git count-objects -vH次に、履歴全体からサイズの大きいblobを並べます。git rev-list --objects --allが全てのオブジェクトとそのパスを返し、git cat-file --batch-checkにフォーマットを渡すと種類・ID・サイズ・パスを1行にできます。%(rest)は入力行の最初の空白より後ろを、そのまま出力に差し込む指定です。
git rev-list --objects --all \
| git cat-file \
--batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
| awk '$1 == "blob"' \
| sort -k3 -n -r \
| head -20%(objectsize)は圧縮前のバイト数です。packの中で実際に占める量は%(objectsize:disk)で見られますが、ここでは「どのファイルが大きいか」の順位を知るのが目的なので、圧縮前のサイズで十分です。同じパスが複数のコミットで別のblobとして並ぶことがあり、その場合は更新のたびに履歴が太っています。
--analyzeで集計済みの報告を得る
git filter-repo --analyzeは、履歴を調べて報告のディレクトリを作る機能です。リポジトリそのものは変更しません。報告は.git/filter-repo/analysis/に出ます。スクリプトの出力処理を見ると、次の4ファイルが作られます。
| ファイル | 中身 |
|---|---|
directories-all-sizes.txt | 中身ディレクトリ単位のサイズ |
extensions-all-sizes.txt | 中身拡張子単位のサイズ |
path-all-sizes.txt | 中身パス単位のサイズ |
blob-shas-and-paths.txt | 中身blobのIDとパスの対応 |
マニュアルは、削除済みのパスを集計したpath-deleted-sizes.txtを例に挙げています。「今のツリーには無いのに履歴で場所を取っているファイル」を探すには、このファイルが最短です。--analyzeを実行するクローンは、次節で作る作業用のものにします。
ミラークローンで作業する理由
git filter-repoは、新しくcloneした直後ではないリポジトリでの実行を、既定で中断します。ローカルにしか存在しない履歴を巻き込む事故を防ぐための確認です。マニュアルは、クローンに--bareか--mirrorを付けてよいこと、ローカルのファイルシステムから複製するなら--no-localを付けること、それ以外のフラグは避けることを勧めています。
git clone --mirror --no-local /path/to/repo repo-mirror.git
cd repo-mirror.git
git filter-repo --analyzeURLからcloneするなら--no-localは要りません。--mirrorを使うと全てのブランチとタグが手元に揃い、作業ツリーを持たないベアリポジトリになります。
落とし穴
確認が通らないからと--forceを足さない
--forceは確認を飛ばして履歴を書き換えます。マニュアルは、既定で実行の最後に古いreflogとオブジェクトを即座に掃除するため、取り消せない操作になると説明しています。通らないときは--forceを足すのではなく、クローンを作り直します。Claude Codeに実行を任せるときも、エラーを避ける目的で--forceを足させない指示を先に入れておきます。
元のクローンで作業を続けたいときは、元のクローンはそのまま残し、ミラークローンを別ディレクトリに置きます。書き換えがうまくいかなければ、ミラークローンを捨てて取り直すだけで済みます。
除去の対象を指定する
対象の指定方法は、除去したいものの性質で選びます。
| 状況 | 指定 | 注意 |
|---|---|---|
| サイズで一括して外したい | 指定--strip-blobs-bigger-than 5M | 注意閾値を超えたblobが全履歴から消える |
| 特定のファイル・ディレクトリを外したい | 指定--invert-paths --path <パス> | 注意複数の--pathは和集合になる |
| 洗い出したblobだけを外したい | 指定--strip-blobs-with-ids <ファイル> | 注意ファイルにはblobのIDを1行ずつ書く |
| 多数のパスをまとめて外したい | 指定--invert-paths --paths-from-file <ファイル> | 注意報告から書き出したパス一覧を渡せる |
--strip-blobs-bigger-thanは5Mや2Gのように単位付きで指定します。便利ですが、現在使っているファイルでも閾値を超えていれば履歴ごと消えます。実行前に、洗い出した上位の一覧に今も必要なファイル(学習済みモデルや、意図して置いた画像など)が混じっていないかを見ます。
パスで外す
パスで指定するときは、--pathだけだと「そのパスだけを残す」動作になります。外したいときは--invert-pathsを併せて付けます。
git filter-repo --invert-paths --path assets/demo.mov報告からまとめて外す
--analyzeの報告から外したいパスだけを書き出して、ファイルで渡す方法がマニュアルにあります。ファイル名を1行ずつ書いたテキストを用意します。
git filter-repo --invert-paths \
--paths-from-file ../files-i-dont-want-anymore.txt複数回に分けて実行しても構いません。.git/filter-repo/already_ranがあると、以降の実行は前回の続きとして扱われ、新クローンの確認も省かれます。段階的に絞り込む使い方は想定された用法です。
小さくなったかを検証する
書き換えの後は、同じ測定をもう一度行います。
git count-objects -vHのsize-packが減っているか- 先ほどの
rev-listとcat-fileのコマンドで、対象のblobが出てこないか git filter-repo --analyzeを再実行して、報告から対象のパスが消えているか
マニュアルも、除去後に--analyzeを再実行して結果を確かめることを勧めています。ただし、size-packが思ったほど減らないこともあります。除去したのが小さなファイルの束なら、全体への寄与が元々小さいためです。その場合は、最初の一覧に戻って次の候補を探します。
git filter-repoは、実行の最後に古いreflogとオブジェクトを掃除します。このためミラークローンでgit gcを別に走らせる手順は、通常は要りません。
force pushして、関係者に再cloneを頼む
ここからは、元のリポジトリと他の人のクローンに影響が出る段階です。
originが外されている点に注意する
git filter-repoは、実行後にoriginのリモートを外します。古い履歴と新しい履歴を混ぜて押し返す事故を防ぐための安全策で、マニュアルは「元のリポジトリに押し返すつもりなら、自分でリモートを足し直す」としています。
git remote add origin <元のリポジトリのURL>
git push --force --mirror origin--mirrorを使うのは、ブランチだけでなく全てのrefを押すためです。書き換えでブランチが消えたときも、--mirrorならサーバー側から消えます。--mirrorは--forceを含む動作ですが、マニュアルも、意図を明示するために両方を並べる書き方にしています。
押す前に確かめることが3つあります。
- 書き換えの作業中に誰かがpushした変更は、force pushで消えます。事前にpushを止めてもらう連絡を入れます
- force pushを禁じているブランチ保護があれば、一時的に外す必要があります
- コードレビューのシステムによっては、
refs/pull/のような特別な名前空間が上書きを許さないことがあります。マニュアルは、こうした参照はサーバー側の対応が要ると述べています
関係者は新しくcloneし直す
他の人のクローンは古い履歴を持ったままです。そこでgit pullしてgit pushすると、書き換え前のコミットと書き換え後のコミットが両方ある状態で履歴が混ざり、消したはずのblobが戻ります。マニュアルは、新しいURLを渡して再cloneしてもらうのが最も確実だとしています。同じ名前のタグも、手元に古いものがあるとgit fetch --tagsでは更新されません。このためタグを含めて、クローンごと取り直してもらいます。
同じgit filter-repoを各自に実行させる方法は、ハッシュが一致する保証がなく勧められません。全員に伝える内容は次の3点です。
- 手元のブランチで未pushの作業があれば、先にパッチやブランチ名を控える
- 古いクローンを削除して、新しくcloneし直す
- 控えた作業は、新しいクローンに
git cherry-pickなどで載せ直す
GitHubの上限とLFSという行き先
GitHubは、50 MiBを超えるファイルをpushすると警告し、100 MiBを超えるファイルをブロックします。それを超えて管理したいファイルは、Git LFSを使います。配布物なら、リポジトリに入れずにリリースの添付ファイルにする方法もあります。リポジトリの大きさは1 GB未満が理想で、5 GB未満を強く推奨するとされています。除去した後に同じ事故を繰り返さないよう、置き場所を決めてから.gitignoreにも書いておきます。
Claude Codeに任せる範囲を設定で決める
測定と検証は、Claude Codeに任せやすい作業です。コマンドの結果を読んで表にまとめ、「この拡張子が全体の何割か」を答える作業なので、読み取り専用のコマンドを事前に許可しておけば、確認の繰り返しが減ります。逆に書き換えとpushは、取り消せないので毎回人間が見る側に置きます。
{
"permissions": {
"allow": [
"Bash(git count-objects *)",
"Bash(git rev-list *)",
"Bash(git cat-file *)"
],
"ask": [
"Bash(git filter-repo *)",
"Bash(git push *)"
],
"deny": [
"Bash(git filter-repo --force *)"
]
}
}ルールは、拒否、確認、許可の順に評価されます。拒否に当たれば、それより狭い許可があっても通りません。上の設定では、git filter-repoの実行は毎回確認が出て、--forceを先頭に付けた形は拒否されます。
この設定は、完全な防壁ではありません。Bashのルールは、Claudeが書いたコマンドの文字列に一致するかを見るだけです。git -C . pushのように書き方を変えた呼び出しには、Bash(git push *)が当たりません。ルールの強制はClaude Code側が行うもので、プロンプトに「force pushしない」と書くだけでは同じ効果は出ません。確実に止めたい操作には、PreToolUseフックで全文を検査するか、サンドボックスを併用します。
Claude Codeにミラークローンの調査を頼むときは、次のように範囲と禁止事項を渡します。
このミラークローンで、履歴上のサイズ上位20のblobを
パス付きで一覧にして。ファイルは変更しないこと。
git filter-repo の実行と push はしない。
候補が決まったら、実行するコマンドだけを提示して止まって。ミラークローンは別のディレクトリにあるので、そのディレクトリでセッションを始めます。一覧の中に「今も使っているはずのファイル」がないかも、あわせて見てもらうと、閾値指定での誤削除を避けやすくなります。履歴から「なぜそのファイルが入ったのか」を調べたいときは、Claude Codeでgit履歴から経緯を調べるの手順が使えます。cloneやfetchがサイズで途中で切れる場合は、Claude Codeセルフホストのgit fetchが巨大リポジトリで切れる原因と設定も参考になります。
まとめ
履歴上の巨大blobは、今のコミットで消しても軽くなりません。size-packと上位blobの一覧で原因を絞り、ミラークローンでgit filter-repoを一度だけ走らせるのが筋です。除去後に--analyzeで検証し、押し返すときはgit remote add originで足し直した上でgit push --force --mirror originを使います。
手間の大半は書き換えの後にあります。関係者への連絡と再cloneの依頼を先に済ませないと、古いクローンから履歴が戻ってきます。Claude Codeには測定と検証を任せ、git filter-repoとgit pushはaskに置いて、人間が実行前に対象を読む構成が、取り返しのつかない操作を避けやすい形です。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。