投稿

誤って commit した重いファイルを git 履歴から除去してリポジトリを軽量化する

誤って commit した重いファイルを git 履歴から除去してリポジトリを軽量化する

状況

ビルド成果物(数 MB の Go バイナリ)を誤って commit して push してしまった。.gitignore で追跡解除しても 履歴には残り続ける ためリポジトリサイズが膨らむ。

クリーンに除去するには履歴の書き換えが必要で、これは破壊的操作なので force-push と各クライアントの再 clone(または reset)が伴う。

主な選択肢

1. git filter-repo(推奨)

git filter-branch の後継として Git 公式が推奨するツール。高速・安全・パスベースの除去が簡単。

# pip / brew でインストール
pip install git-filter-repo

# 特定ファイルを全履歴から削除
git filter-repo --invert-paths \
  --path path/to/big-file-1 \
  --path path/to/big-file-2

# remote が自動で外れるので再追加
git remote add origin git@github.com:org/repo.git

# 強制 push
git push --force-with-lease origin main

メリット: シンプル、高速、複雑な書き換えにも対応。 デメリット: 別途インストール必要、デフォルトで origin remote を消す(再追加が必要)。

2. BFG Repo-Cleaner

巨大リポジトリ向けの高速ツール(Java 製)。サイズ閾値での一括削除が得意。

# 100KB 以上のファイルを全部除去
bfg --strip-blobs-bigger-than 100K
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force

メリット: 100MB のリポでも数秒、サイズ指定が便利。 デメリット: Java 必要、細かいパス指定は filter-repo の方が柔軟。

3. git filter-branch(非推奨)

Git 標準だが遅くてバグが多く、Git 公式も filter-repo を推奨している。今は使う理由がない。

4. 直近 commit のみなら git rebase

push 済みでも自分しか触っていないなら、interactive rebase で当該 commit を drop → force-push でも可。1 コミットだけ消したい場合これが一番軽い。ただし上に commit が積まれていると、衝突リスクが上がる。

実行手順(filter-repo)

1. バックアップを取る

履歴書き換えは取り返しがつかないので必ずミラークローンを取る。

git clone --mirror /path/to/repo /tmp/repo-backup-$(date +%s).git

2. remote URL を控える

filter-repo は実行後に origin を消すので、URL を保存しておく。

git remote get-url origin > /tmp/origin-url.txt

3. filter-repo 実行

git filter-repo --invert-paths \
  --path path/to/file-a \
  --path path/to/file-b \
  --path path/to/file-c

過去に同じリポで filter-repo を実行したことがある場合、already_ran チェックに引っかかって実行が止まる。--force でも対話プロンプトが出るので、echo "Y" | でパイプするか stdin を渡す。

echo "Y" | git filter-repo --force --invert-paths --path ...

4. remote 再追加して force-push

git remote add origin "$(cat /tmp/origin-url.txt)"
git push --force-with-lease origin main

--force-with-lease は通常の --force よりも安全で、リモートが自分の認識する状態と異なる場合は push を拒否する。

5. ローカル GC で容量回収

git reflog expire --expire=now --all
git gc --prune=now --aggressive

これで dangling object(参照されなくなった古いオブジェクト)が物理的に削除される。du -sh .git で前後比較できる。

副作用と注意点

全 commit の SHA が変わる

filter-repo は安全のため全 commit を rewrite する(対象ファイルが含まれない commit も含めて)。これにより:

  • ローカルの全ブランチが remote と SHA レベルで divergence する(内容は同じ)
  • push 済みの古い SHA への参照(PR 上のリンクなど)は壊れる可能性がある
  • 共同作業者は再 clone が必要になる

他のブランチの扱い

force-push するのは履歴を整理したいブランチ(多くの場合 main)のみで OK。他の feature ブランチに対象ファイルが含まれていなければ、それらは触らない方が安全。

ただしローカル側では filter-repo が全 ref を rewrite してしまうので、再 fetch すると [behind] 表示が出る。必要なブランチだけ git reset --hard origin/<branch> で揃え直すか、リポジトリを再 clone する。

リモート側の物理削除

force-push しても、削除した古い commit は GitHub などのホスティング側の pack に残る。即時削除したい場合はサポートに連絡が必要だが、通常はホスティング側の自動 GC でいずれ消えるので待てばよい。

セキュリティ事故(誤って secret を commit)の場合は即削除を依頼すること。

確認コマンド

履歴書き換え前後で、対象ファイルが reachable な commit に含まれていないか確認する。

# 全 ref から到達可能な範囲で、ファイル追加 commit を探す
git log --all --diff-filter=A --name-only --oneline \
  | grep -E "target-file-name"

# 結果が空なら除去成功

# 特定 commit が到達可能か
git branch -a --contains <old-sha>
# 何も出なければ unreachable

まとめ

  • 重いファイルを履歴から消すには git filter-repo が現代の標準
  • 必ずバックアップ → 実行 → force-push → gc の順
  • 全 commit の SHA が変わるので共同作業者・PR 参照に注意
  • private リポ・単独作業なら影響範囲が限定的で実行しやすい
  • 完全削除はホスティング側の GC を待つかサポート依頼

トレンドのタグ