やりたかったこと(症状)

study-notesという個人用リポジトリに、検証用のサンプル動画ファイルassets/demo.mp4(約105MB)を追加してコミットし、そのままGitHubへgit pushしようとした。

git add assets/demo.mp4
git commit -m "add demo video for docs"
git push origin main

コミット自体は問題なく成功したが、pushが途中で止まり、次のようなエラーで拒否された。

remote: error: GH001: Large files detected. You may want to try Git Large File Storage - https://git-lfs.github.com.
remote: error: Trace: 3f5a1e2b8c9d4a7f6e2c1b0a9d8e7f6c5b4a3d2e
remote: error: See http://git.io/iEPt8g for more information.
remote: error: File assets/demo.mp4 is 105.34 MB; this exceeds GitHub's file size limit of 100.00 MB
To github.com:example-user/study-notes.git
 ! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to 'github.com:example-user/study-notes.git'

git commitは通っているのにpushだけが拒否される、という挙動に最初は戸惑った。ローカルのコミット自体は正常に見えるため、何が悪いのか一見分かりにくい。

環境

  • OS: Ubuntu 22.04.4 LTS
  • Git: 2.43.0
  • リモート: GitHub.com(プライベートリポジトリ、SSH接続)
  • Git LFS: 未導入(今回問題の原因の一つ)
  • 該当ファイル: assets/demo.mp4(105.34 MB)

試したこと

まず、pushが失敗しただけで、コミット自体は取り消せばよいだろうと考え、最新コミットで該当ファイルを削除する追加コミットを作って再度pushした。

git rm assets/demo.mp4
git commit -m "remove large demo video"
git push origin main
remote: error: File assets/demo.mp4 is 105.34 MB; this exceeds GitHub's file size limit of 100.00 MB
To github.com:example-user/study-notes.git
 ! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to 'github.com:example-user/study-notes.git'

ワーキングツリーからもインデックスからもファイルは消えているのに、まったく同じエラーで拒否され続けた。ここで、pushが拒否しているのは「現在のファイル一覧」ではなく「これから送信しようとしているコミット履歴に含まれるすべてのオブジェクト」だと気づいた。実際、git rmで削除するコミットを新たに積んでも、1つ前のコミットにはまだ105MBのdemo.mp4のBlobが残ったままで、push時にはその履歴も含めてリモートへ送信されようとしていた。

念のため、履歴のどこにそのファイルが存在するかを確認した。

git rev-list --objects --all | grep demo.mp4
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0 assets/demo.mp4

やはりオブジェクトとしては履歴中に残存しており、単にワーキングツリーから消しただけでは不十分だと確認できた。

原因

GitHubは1ファイルあたり100.00 MBを超えるBlobを含むpushをサーバー側のpre-receiveフックで一律拒否する仕様になっている(50MB超では警告のみで通過するが、100MB超は必ず拒否される)。この判定は「pushしようとしているコミット群に含まれる全オブジェクト」に対して行われるため、最新コミットでファイルを削除しても、過去のコミットにそのファイルのBlobが残っている限りpushは通らない

今回のケースは、git commit -m "remove large demo video"によって作業ディレクトリ上のファイルは消えたが、git log上は「追加したコミット」と「削除したコミット」の両方が履歴に残っており、pushはその両方を送ろうとしていた。つまり履歴を書き換えない限り、この105MBのBlob自体はリポジトリのオブジェクトとして送信対象から外れないという、Git・GitHub双方の仕様どおりの挙動だった。

解決方法

1. 問題のオブジェクトが含まれるコミットを特定する

git log --oneline --all -- assets/demo.mp4
7c8d9e0 remove large demo video
4b5c6d7 add demo video for docs

過去のコミットにBlobとして残っていることを再確認する。

2. 安全のため作業用に別ディレクトリへ再クローンする

履歴書き換えは元に戻せない操作なので、既存の作業ディレクトリを直接いじらず、別の場所にクリーンな状態で作業する。

git clone git@github.com:example-user/study-notes.git study-notes-cleanup
cd study-notes-cleanup

3. git filter-repoで履歴から該当ファイルを完全に削除する

git filter-repogit filter-branchの後継として公式に推奨されているツール)を使い、全履歴からassets/demo.mp4を取り除く。

sudo apt install git-filter-repo
git filter-repo --path assets/demo.mp4 --invert-paths

--invert-pathsを付けることで「指定パス以外を残す」ではなく「指定パスだけを履歴から除去する」動作になる。

4. 履歴からオブジェクトが消えたことを確認する

git rev-list --objects --all | grep demo.mp4

出力が空になっていれば、全コミットからBlobが除去できている。

5. force pushでリモートの履歴を書き換える

自分一人だけで使っているブランチであることを確認したうえで、書き換え後の履歴でリモートを上書きする。

git push origin --force --all
git push origin --force --tags
Enumerating objects: 42, done.
...
To github.com:example-user/study-notes.git
 + 4b5c6d7...9f8e7d6 main -> main (forced update)

今度はpre-receive hook declinedが出ず、正常にpushが完了した。

6. 今後のためにGit LFSで大容量ファイルを管理する

同じ問題を繰り返さないよう、動画・バイナリなど大きくなりがちなファイルはGit LFSで追跡する運用に切り替えた。

git lfs install
git lfs track "*.mp4"
git add .gitattributes
git commit -m "track mp4 files with Git LFS"

動作確認

git push origin main
Everything up-to-date

新たにassets/demo.mp4相当のファイルを追加し直して確認したところ、.gitattributesの設定どおりLFSポインタとして扱われ、pushも問題なく完了した。

git add assets/demo.mp4
git commit -m "re-add demo video via LFS"
git push origin main
Uploading LFS objects: 100% (1/1), 105 MB | 4.2 MB/s, done.
Enumerating objects: 4, done.
...
To github.com:example-user/study-notes.git
   9f8e7d6..1a2b3c4  main -> main

pre-receive hook declinedが出なくなり、正常にリモートへ反映されたことを確認できた。

ハマったポイント

  • git rmでファイルを削除するコミットを積んでも、それは「新しいコミットを追加している」だけで、過去のコミットに残ったBlobは消えない。pushはそのコミット群すべてを送信対象にするため、削除コミットだけでは根本解決にならなかった。
  • git filter-repoはデフォルトで「フレッシュなクローンでない限り実行を拒否する」安全策が入っている。既存の作業ディレクトリでいきなり実行しようとすると止められるので、素直に再クローンしてから作業するのが結局早かった。
  • 履歴書き換え後の--forcepushは、共同作業しているリポジトリでは他の人のローカル履歴と食い違いを起こす。今回は自分専用のリポジトリだったため実行したが、共有ブランチであれば事前に必ずチームへ共有してから行うべき操作だと理解した。
  • GitHubのファイルサイズ制限は「50MB超で警告、100MB超で拒否」という2段階になっており、警告段階を見逃して放置すると今回のように後から100MBを超えて詰まることがある。

よくある質問

Q: 最初からGit LFSを使っていればこの問題は起きませんでしたか? 起きなかったはずです。LFSで追跡されているファイルは実体ではなくポインタファイルとしてGitの通常履歴に記録されるため、100MB制限の対象になりません。大きくなりがちな種類のファイルは、追加する前に.gitattributesでLFS管理下に置いておくのが安全です。

Q: git filter-repoBFG Repo-Cleanerはどちらを使うべきですか? 現在はGit公式ドキュメントでもgit filter-branchよりgit filter-repoが推奨されています。今回のような「特定パスを履歴全体から除去する」用途であればどちらでも対応可能ですが、開発が活発でオプションも分かりやすいgit filter-repoを選びました。

Q: 50MB〜100MBのファイルはpushできますか? できます。ただしpush時に警告メッセージが表示されるため、リポジトリが大きくなりすぎないよう、警告が出た時点でLFS移行を検討するのがよいと感じました。

関連記事