😮‍💨

誤ってGitHubに上げたファイルを履歴から抹消する方法

2024/12/22に公開

個人開発のアプリのGit管理で思わぬ問題に気づいて対応したので、ログとして残します。
最初からコミット内容をちゃんと確認すれば防げた初歩的なミスですが、そこは目をつぶっていただければ幸いです🤗

遭遇した事象

  • 😇 昔、誤って50MB以上のファイルをコミットし、GitHubにpushしてしまった
  • 😱 リリースビルドのたびに新しく生成されるバイナリファイルだったため、リリースするたびにコミットされていた
  • 😮‍💨 見つけたタイミングで .gitignore に追加して、ファイルをgit管理から削除した
  • 🤔しかし、気づけばgit pull, git push, git cloneが長時間かかるようになった。試しに新しくcloneしたら、かかった時間はなんと7分。

原因

実はgitでは削除済みを含む全ファイルの過去のすべてのバージョンを保存しています。
そのため、 .gitignore に追加してgit管理から削除しただけでは、完全に削除されないことを知りました。

https://www.geeksforgeeks.org/how-to-remove-a-large-file-from-commit-history-in-git/

Simply removing a large file through a commit won't truly eliminate it. Git stores all versions of files in its history, even deleted ones, to allow for easy recovery. This can lead to wasted disk space.

完全に削除したい場合、gitのすべての履歴からそのファイルを抹消する必要があります。

Git履歴が肥大化しているか確認する方法

リポジトリの ./.git/objects/pack 内を見てみます。
pack-ハッシュ値.pack が実際のコミット内容等が含まれるファイルなのですが、これがなんと3.9GBまで膨れ上がっていました。

ls -lhg
total 8270456
-r--r--r--@  1 staff   316K Dec 22 13:49 pack-ハッシュ値.idx
-r--r--r--@  1 staff   3.9G Dec 22 13:49 pack-ハッシュ値.pack

解決手段の検討

検索すると、いくつかのツールが見つかりました。

  • git filter-branch
  • BFG Repo Cleaner
  • git filter-repo

最終的には git filter-repo を採用しました。

(最初に git filter-branch を使おうとしたところ、公式に今は git filter-repo が推奨されていると表示されました。また、BFGもdeprecatedだという記載を見たので使うのをやめました。)

⚠️ 最終的にはmainブランチにforce pushする危険な操作になるので、個人開発以外であれば他の方法がないか検討すると思います。個人開発の場合も、別のフォルダにバックアップを取っておくなど慎重に進めましょう!

git filter-repoで解決するまで

インストール

公式のインストール手順としては、以下で示されているPythonのファイルを任意の場所に置くだけです。

https://github.com/newren/git-filter-repo/blob/main/INSTALL.md#simple-installation

ただ、このままだと以下の呼び出し方になります。

python3 git-filter-repo --analyze

本当は以下のように呼び出せたほうが便利です。

git filter-repo --analyze

その場合 $PATH に置けばできるとの記載があるので、 /usr/local/bin/ に置き、executableに設定することにしました。

cp git-filter-repo /usr/local/bin/
chmod +x /usr/local/bin/git-filter-repo

これだけで使えます!

重いファイルを特定する

以下で分析を実行できます。

git filter-repo --analyze

分析結果が .git/filter-repo/analysis フォルダに出力されます。

ls -lhg
total 760
-rw-r--r--@  1 staff   3.3K Dec 22 10:31 README
-rw-r--r--@  1 staff   331K Dec 22 10:31 blob-shas-and-paths.txt
-rw-r--r--@  1 staff   3.8K Dec 22 10:31 directories-all-sizes.txt
-rw-r--r--   1 staff   580B Dec 22 10:31 directories-deleted-sizes.txt
-rw-r--r--@  1 staff   1.5K Dec 22 10:31 extensions-all-sizes.txt
-rw-r--r--   1 staff   188B Dec 22 10:31 extensions-deleted-sizes.txt
-rw-r--r--   1 staff    16K Dec 22 10:31 path-all-sizes.txt
-rw-r--r--@  1 staff   2.8K Dec 22 10:31 path-deleted-sizes.txt
-rw-r--r--   1 staff   524B Dec 22 10:31 renames.txt

blob-shas-and-paths.txt を見た結果、dSYM(debug symbolsのファイル)がしばらくの間コミットされていたことがわかります。
毎回、67MBがコミットされていました😱 原因特定です。

=== Files by sha and associated pathnames in reverse size ===
Format: sha, unpacked size, packed size, filename(s) object stored as
  45d9c6b9074c86b821f83c67cfcb05a1eed4bbd6   67440966   66902463 ios/my-app.app.dSYM.zip
  3da004e42728ec9fa2fa7fa871340d2c29c96d53   67441041   66900455 ios/my-app.app.dSYM.zip
  4aa3f5d4d1f6159a9b3c3fe4c8dbada03d79076b   67230820   66693448 ios/my-app.app.dSYM.zip
  4e488eeb2136ba5b093c657735d5d148ba34aa29   67230199   66692453 ios/my-app.app.dSYM.zip
  4ea4904493a0b719a69a621fb16b491395639b8f   67230815   66692280 ios/my-app.app.dSYM.zip
  fad561b05823f9b2e39acf073498d58a8a28ff55   67019666   66483395 ios/my-app.app.dSYM.zip
# …続く

重いファイルを消し去る

以下でできます。

git filter-repo --invert-paths --path ios/my-app.app.dSYM.zip
  • --path ios/my-app.app.dSYM.zip: ファイルパスを指定
  • --invert-paths: 上記ファイルパス以外をgit履歴に残すようにする

実行時のログ:

$ git filter-repo --invert-paths --path ios/my-app.app.dSYM.zip
NOTICE: Removing 'origin' remote; see 'Why is my origin removed?'
        in the manual if you want to push back there.
        (was git@github.com:risafj/my-app.git)
Parsed 4052 commits
New history written in 0.14 seconds; now repacking/cleaning...
Repacking your repo and cleaning out old unneeded objects
HEAD is now at d1d2c95 Merge pull request #709 from risafj/fix-large-files
Enumerating objects: 10853, done.
Counting objects: 100% (10853/10853), done.
Delta compression using up to 12 threads
Compressing objects: 100% (3341/3341), done.
Writing objects: 100% (10853/10853), done.
Total 10853 (delta 7178), reused 10843 (delta 7177), pack-reused 0
Completely finished after 0.27 seconds.

これで手元のgit履歴はすべて書き変わりました!

最後にremoteにpushするわけですが、ここで注意点があります。
git filter-repo を実行すると、書き換えた履歴を軽率にpushしてしまわないようにという配慮のもと、勝手にremoteの設定が外されます。
そのため、このままpushすることはできません。

もとのリポジトリの全ブランチにいきなり破壊的なforce pushするのではなく、新しいGitHubリポジトリを作ってそちらにpushすることをおすすめしているようです。
安全のために私も一旦そのようにしました。

詳しくは公式docsの "Why is my origin removed?" を参照してください。
https://htmlpreview.github.io/?https://github.com/newren/git-filter-repo/blob/docs/html/git-filter-repo.html

別リポジトリで確認したり、手元でビルドしたりして安全だとわかった後、私は最終的に元のリポジトリにもforce pushしました。

git remote add origin リモートリポジトリ
git push --force --branches --tags --prune

結果を見てみる

操作前は3.9GBだったpackファイルが、なんと1000分の1の3.5MBまで下がりました。
おかげで快適にgit操作ができるようになりました🎉

ls -lhg
total 7808
-r--r--r--@  1 staff   298K Dec 22 20:15 pack-ハッシュ値.idx
-r--r--r--@  1 staff   3.5M Dec 22 20:17 pack-ハッシュ値.pack

最後まで読んでいただきありがとうございました!

Discussion