# git rm --cachedで.gitに26MBを残した — ワーキングツリーはクリーンで、履歴はそうではなかった

> git rm --cached が消すのはインデックスからのパスであり、過去のコミットからではありません。その違いを計測する方法と、filter-repo でコミット済みのビルド成果物を取り除く手順を解説します。

- Source: https://oharu121.com/ja/blog/git-rm-cached-working-tree-vs-history-filter-repo/
- Published: 2026-08-27T13:59:31+09:00
- Tags: Git

---
**要点**

- `git rm --cached` は、パスをインデックスと次のコミットから取り除きます。すでに存在するコミットは、それでも blob を参照したままです。
- クリーンな `git status` と小さいワーキングツリーは、リポジトリのサイズについて何も証明しません。証明するのは `du -sh .git` です。
- ファイル名を指定した `.gitignore` のエントリは、同じ語幹を持つディレクトリまではカバーしません。
- `git filter-repo --path X --invert-paths` は、すべてのコミットからパスを取り除きます。すべてのコミット ID が変わりますが、最初の push より前ならコストはゼロで、後なら大きなコストになります。
- バックアップ用のバンドルは、いま削除したばかりのバイトをそのまま保存しています。書き換えを検証したら削除してください。

## はじめに

あるプロジェクトのソースを、`.venv` などを除いた tgz として渡してほしいと頼まれました。それ自体は2分で終わる作業のはずでしたが、実際にパッケージ化してみて問題が表面化しました。リポジトリは **26 MB** あり、そのほとんどは最初のコミットで誰にも気づかれないままコミットされていたビルド成果物だったのです。

エージェントは `git rm --cached` でそれを削除し、ディレクトリを `.gitignore` に追加し、リポジトリが26 MBから292 KBになったと報告しました。**その主張は誤りでした**。そしてこれは、この種の間違いがそのまま生き延びる典型的な経路です。追跡対象のファイルは実際に292 KBまで縮んでいましたが、誰も `.git` そのものを見ていませんでした。`.git` は依然として26 MBのままだったのです。この blob は6つのうち5つのコミットから到達可能な状態にあり、放っておけばリポジトリと永遠に一緒に運ばれ続けるところでした。

本当の修正は `git filter-repo` で、かかった時間は1秒未満でした。この記事では、この成果物がどう紛れ込んだのか、一見正しく見える削除コマンドがなぜそれを削除しなかったのか、計測によってその違いをどう見分けるのか、そして安く直せる猶予が最初の push で閉じてしまう理由を扱います。

## 26 MBが紛れ込んだ経緯 — .gitignore はファイルにマッチしても、ディレクトリにはマッチしなかった

このプロジェクトのデプロイツールは、リポジトリのルートに2つのものを書き込みます。設定ファイルの `.bedrock_agentcore.yaml` と、ベンダリングされた依存関係の zip を格納するビルドディレクトリの `.bedrock_agentcore/` です。

エージェントが書いた `.gitignore` は、そのうち片方だけを列挙していました。

```text title=".gitignore"
.venv/
.cognito.env
.bedrock_agentcore.yaml
__pycache__/
```

**gitignore のパターンは、パスに対してマッチするのであって、名前の語幹に対してではありません。** `.bedrock_agentcore.yaml` は、まさにその名前を持つファイルにマッチします。これは `.bedrock_agentcore/` について何も述べていません。これは別のパスなので、そのディレクトリと26 MBのzipは一度も無視されず、最初の `git add -A` で取り込まれました。

**何も文句を言いませんでした。** コミットは成功し、テストは通り、リポジトリはその後何時間も問題なく動作しました。

## git rm --cached と、成功に見えた数字

アーカイブをパッケージ化する際、エージェントはこの成果物を見つけ、いかにも正しそうなやり方で削除しました。

```bash
git rm -r --cached .bedrock_agentcore
printf '.bedrock_agentcore/\n' >> .gitignore
git commit -m "Stop tracking the AgentCore build artifact directory"
```

そして計測し、その結果を修正として報告しました。

```bash
git ls-files -z | xargs -0 du -ch | tail -1
#   292K  total
```

アーカイブ自体は `git archive` で作られていました。これはオブジェクトストアではなくツリーを歩くコマンドなので、サイズは **83 KB** となり、本当にクリーンでした。目に見えるものはすべて、問題は解決したと告げていました。

**その計測は、そもそも見るべきものを間違えていました。** `git ls-files` が一覧するのは、*今* 追跡されているものです。それ以前のコミットに何が含まれているかについて、`git ls-files` は何の判断も下しません。`git status` も、ワーキングツリーのサイズも同じです。

## 実際にはまだ何が残っていたのか

`git rm --cached` がすることは1つだけです。**パスをインデックスから取り除き、次のコミットにはそれが含まれないようにする**ことです。これは履歴を操作するコマンドではありませんし、そのように振る舞うつもりもありません。コミットは不変なので、**すでにその blob を参照していた5つのコミットは、その後も参照したままでした**。そして、いずれかのコミットから到達可能なオブジェクトは、git が保持し続けるオブジェクトです。

これを決着させるコマンドは2つです。1つはオブジェクトストアの大きさを計測し、もう1つは blob がまだ到達可能かどうかを尋ねます。

```bash
du -sh .git
#   26M  .git

git rev-list --objects --all | grep dependencies.zip
#   de67db72dca595f3e22bd20b3ac5c77449734dd7 .bedrock_agentcore/.../dependencies.zip
```

つまり、このリポジトリは292 KBのファイルを、26 MBのリポジトリの中に抱えていたことになります。**この2つの数字の差こそがバグのすべて**であり、ワーキングツリーについて報告するどのコマンドからも見えません。

*Figure — WorkingTreeVsHistory: インデックスはクリーンでした。6つのうち5つのコミットは、それでも blob を指したままでした。*

## 猶予期間 — まだ何も push されていなかったこと

これをそのまま放置せず、すぐに直す価値があると判断させた事実が2つありました。

1つ目は、このリポジトリにリモートが一切なかったことです。ローカルで作成され、一度も push されたことがなかったので、他にクローンは存在せず、ディレクトリの外部からコミット ID を参照しているものも何もありませんでした。

2つ目は、思い込みではなく確認する価値がありました。というのも、同じコードが feature branch として共有リポジトリにも提出されていたからです。もしこの成果物がそちらにも渡っていたら、はるかに厄介な話になっていたはずです。

```bash
git rev-list --objects --all | grep -iE "dependencies\.zip|bedrock_agentcore"
#   (no output)
```

クリーンでした。このブランチが追加したのは合計183 KBで、リポジトリをコピーするのではなく、ソースファイルをコピーして組み立てられていたからです。

*Figure — RepairWindow: 同じ書き換えでも、コミット ID が非公開でなくなる瞬間を境に、コストがまったく変わります。*

この組み合わせが、コストの安いケースです。**history rewrite は、それが触れた地点より後のすべてのコミット ID を変えます**。それらの ID を誰も参照していなければコストはゼロですが、同僚がそれをチェックアウトしていたり、pull request がそれに対して開かれていたり、CI がその ID を記録していたりすれば、コストは大きくなります。私は以前、まったく同じ理由で書き換えを見送ったことがあります。[回収できる容量が0.8 MBで、コミット ID がすでに25個のタグと34件のマージ済み pull request に握られていたときです](/ja/blog/git-gc-loose-objects-vs-filter-repo-history-rewrite/)。今回は26 MBで、しかも何も push されていませんでした。同じ問いに対して、正反対の答えが出たわけです。

## filter-repo で書き換える

`git filter-repo` は `filter-branch` の後継であり、現在の git ドキュメントが指し示しているのもこちらです。今回の操作は、すべてのコミットにわたるパスの削除です。

```bash
# 先にバックアップを取る: リモートがないので、このディレクトリが唯一のコピーです。
git bundle create ../project-backup.bundle --all

git filter-repo --path .bedrock_agentcore --invert-paths --force
```

`--path` はパスを選択し、`--invert-paths` はその選択を反転させて、指定したもの *以外* をすべて残します。`--force` が必要なのは、filter-repo が本来はクローンしたての状態を前提としているのに対し、今回は作業中のリポジトリそのものに対して実行したからです。

実行は速く、何をしたかもきちんと出力します。

```text
Parsed 6 commits
New history written in 0.07 seconds; now repacking/cleaning...
Completely finished after 0.20 seconds.
```

## 何が変わり、何が変わらなかったのか

| | 変更前 | 変更後 |
| --- | --- | --- |
| `.git` | 26 MB | **236 KB** |
| 追跡ファイル数 | 23 | 23 |
| 追跡サイズ | 292 KB | 292 KB |
| コミット数 | 6 | 6 |
| `dependencies.zip` が到達可能か | はい | **いいえ** |

すべてのコミットはメッセージそのままに生き残り、ワーキングツリーには一切手が加えられていませんでした。これはパッケージを実際にインポートして抜き打ちで確認済みです。変わったのは、約束どおりすべてのコミット ID だけでした。

見た目の上で1つ、想定しておくべき変化があります。この成果物を削除することだけを目的としていたコミットが、今では何も削除しなくなったのです。書き換え後の履歴のどの時点にも、その成果物自体が存在しなくなったからです。そのコミットに残っているのは、`.gitignore` の行だけです。

```text
6d88cf7 Stop tracking the AgentCore build artifact directory
 .gitignore | 7 ++++---
```

それでもメッセージ自体は意図を正確に説明しているので、そのまま残しました。これを直すには、機能的な利益がないまま、もう一度書き換えを行うことになります。

最後にもう1つ、ちょっと笑ってしまう罠があります。**バックアップ用のバンドルは25 MBあります**。これは blob を含む履歴を、そのまま忠実に保存しているからです。これをリポジトリの隣に置いたままにするというのは、まさに取り除こうとしたバイトそのものを保持し続けるということです。書き換えを検証したら削除してください。

```bash
git bundle verify ../project-backup.bundle
#   The bundle records a complete history.
```

## まとめ

`git rm --cached` は履歴を操作するコマンドではありませんし、そう振る舞うつもりもありません。パスをインデックスから取り除き、以降のコミットからそれを除外するというもので、想定されている用途にはまさに正しい動作ですが、すでにコミットされてしまったファイルに対してはまったく不十分です。

この間違いがそのまま居座ってしまう理由は、手軽に測れるものがすべて、事後になってあなたに同意してしまうからです。`git status` はクリーンで、ワーキングツリーは小さく、`git archive` はきれいな tarball を作ります。**ただ1つ食い違う数字が `du -sh .git`** で、これだけがクローン時に実際にダウンロードされるものを言い当てています。

大きなファイルがコミットされてしまっていたら、何よりも先に、何か push 済みかどうかを確認してください。**このひとつの事実だけで**、修正が1秒未満のローカルコマンドで済むのか、それとも調整が必要な問題になるのかが決まります。

## 参考リンク

- [git rm のドキュメント。--cached がインデックスに対して何をするかを含む](https://git-scm.com/docs/git-rm)
- [git filter-repo。git プロジェクトが filter-branch の代わりに推奨しているツール](https://github.com/newren/git-filter-repo)
- [gitignore のパターン形式。パターンがどうパスにマッチするか、末尾のスラッシュがどうディレクトリへのマッチに制限するか](https://git-scm.com/docs/gitignore#_pattern_format)
- [git bundle のドキュメント。リポジトリのオフラインバックアップを作成・検証する方法](https://git-scm.com/docs/git-bundle)
