gitリポジトリに入れた巨大なファイルの履歴を削除する

前回のエントリ時にはかなり余裕のあるクラウドにリモートを移動したので,ほかの関連ファイルも一括でgit管理してしまおうと思っていたのだが,割と面倒なことになった.関連ファイルには10GBを超える動画ファイルがいくつか含まれており,それをプッシュしたところ,40GBを超える巨大なpackファイルなるものが生成された.リモートリポジトリの容量も巨大化した.

問題は,巨大な動画ファイル自体をローカルから削除してコミットしてプッシュしても,packファイルはほとんど変化せず,容量もまったく減らなかったことである.考えてみれば,任意の時点のファイルを復元できるということはどこかにその情報が保持されているわけで,いかに圧縮しようと,可逆性を保っている限りある程度のサイズになるのはやむをえないはずである.

巨大な過去のコミットたちと,それらを保持していると思われるpackファイル.コミットにもファイルとしての実体があるようだ.これまで考えたこともなかった.

$ ls -laR /path/to/repo.git/| awk '{if ($5 > 100000000) print $0}'
 -r--r--r--    1 lingvisticae  staff  106954354 Oct 17 11:06 e8c69bbf05a87871c5819e80ad3ef1e6e20f37
 -r--r--r--    1 lingvisticae  staff  104546325 Oct 17 11:06 327871bad21d092f5096216943c5aec5ad4e6e
 -r--r--r--    1 lingvisticae  staff  693927392 Oct 17 11:07 27d5f7983a2d56b236b0828392d3e57aaac58f
 -r--r--r--    1 lingvisticae  staff  162961037 Oct 17 11:06 7e30e6b07a5d3163b3d00c40b711b8783085e4
 -rwxr-xr-x@   1 lingvisticae  staff   2039711683 Oct 13 10:25 pack-22a545fc7ddb1c86f7b43a24a8afe6bc1f99e8c7.pack
 -rwxr-xr-x@   1 lingvisticae  staff   2059296845 Oct 17 10:53 pack-3c10cef7a46302b019aeeaad360b77c3aaef9b0f.pack
 -rwxr-xr-x@   1 lingvisticae  staff   2042554513 Oct 16 12:32 pack-663d10973d4894882a90b44f9f36a9fae6a75083.pack
 -rwxr-xr-x@   1 lingvisticae  staff   2042555488 Oct 16 12:49 pack-a3d364ff5d91f08a7658dd9cc9d3f6f2b5070472.pack
 -r--r--r--    1 lingvisticae  staff  48236415702 Oct 17 13:55 pack-b77d47905579c9d82cabcaed2a81c200ce82f28e.pack

直接の問題としては単一のファイルが巨大化したことによりクラウドのアップロード上限に達した(OneDriveは15GBまで)ためにデバイス間共有ができなくなったことだが,ローカルとリモートでの合計がもとの動画のサイズの3倍超に膨れ上がったことによるディスク容量の圧迫も無視できない問題になった.

ローカルにも巨大なpackファイルがある.tmp_pack_7fYZnaというのはコミットかプッシュの過程で生成されるファイルで,packファイルと同等のサイズがあり,このファイルの生成途中でハードウェアのディスク容量の上限に達してしまい,操作が完了できなくなった.

$ ls -laR repo/| awk '{if ($5 > 100000000) print $0}'
 -r--r--r--  1 lingvisticae  staff  51481809799 Oct 17 17:25 pack-89968939d94286ec10a24a9a112f8e1eee02e842.pack
 -r--r--r--  1 lingvisticae  staff  19198967808 Oct 17 16:49 tmp_pack_7fYZna

調べてみると,Githubにも単一ファイルのアップロード上限があるらしく,大きいファイルの対策に迫られることは割とあることのようだった.とはいえ10GB超のファイルを差分管理しようとする愚か者は見当たらなかったが…

qiita.com
sutara79.hatenablog.com
confluence.atlassian.com

ということでこれらの記事を参考に,適当にファイルを逃がして容量を空け,巨大なファイルの履歴を消していく.

手順1

まず上記のAtlassianのサイトから,git_find_big.shというシェルスクリプトをダウンロードする.どのファイルが問題なのか明らかな場合には必ずしもやらなくていい.ダウンロードしたら,ローカルリポジトリの一番上に移動して実行する.

cd /path/to/repo
bash /path/to/git_find_big.sh

おそらくpackファイルが巨大であればあるほど走査に時間がかかるが,しばらくすると大きい方から上位10件が表示される.

$ bash /path/to/git_find_big.sh 
All sizes are in kB's. The pack column is the size of the object, compressed, inside the pack file.
size     pack     SHA                                       location
7996160  7985664  7e163546b1039166bc677445efb8a5018b46907b  DATA/151205/20151205132024_1.mpeg
7992388  7981966  f8029a9997d9ac6e36dafc09b748bde9819ee99e  DATA/151205/20151205132024_2.mpeg
7991920  7981168  7adbbf4a00c4490b1ceeb51919cd79af073e8a0c  DATA/151205/20151205132024_3.mpeg
6902880  6709183  cbb79aa49d554d61456f8bc92b8cd99a4ef54534  DATA/151205/20151205193243.mpeg
3908014  3908504  db8bb6f781757ca620949b5803845d4fe63a6f31  DATA/運転練習データ/GP010006.MP4
2707417  2694039  f4f1b570d8458e3dbec285ef1bdc2ffaaca49e69  DATA/task/T014_018_eaf/T014_018_MIX.mp4
2072616  2072176  4bb8a6c45e727b5c36852d463593ec43b065b510  DATA/task/T001_002_eaf/T001_002_MIX.mp4
828483   827771   7c6004efcb3546b4fcc4685c6626f35fa268d29c  DATA/割り込み?/割り込み.mov
727311   620069   03c97ee54cce7f0e3cf790936fdff8947a75439b  DATA/task/T014_018_eaf/T014_018_IC0A.wav
727311   239959   225fd98a5874a46a26270794f092ed75ca984e70  DATA/task/T014_018_eaf/T014_018_IC02.wav

どのファイルを歴史から抹消すべきか判明したので,消去のコマンドを実行する.ローカルレポジトリの一番上のままで,

$ git filter-branch --index-filter 'git rm --ignore-unmatch DATA/151205/20151205132024_1.mpeg' --tag-name-filter 'cat' -- --all

詳しいコマンドの解説はこちらにあるが,git filter-branch <command>で対象となるコミットにcommandを実行,そのコマンドがgit rm --ignore-unmatch <file>である.その他のオプションは,コミット名やタグを上書きするなど.

実行後しばらくはファイル数と時間が書き換わって進捗が表示されるが,終わり際には行が増えるようになる.ちなみにファイルが巨大で,かつ自動コミットのせいでコミットの数も膨大なので,一つのファイルの履歴を消すのに何時間もかかった.

$ git filter-branch --index-filter 'git rm --ignore-unmatch DATA/151205/20151205132024_1.mpeg' --tag-name-filter 'cat' -- --all
Rewrite 550f5fb201c4e0e9b3f896e3a9140247b528ecf5 (2062/2098) (6483 seconds passed, remaining 113 predicted)    rm 'DATA/151205/20151205132024_1.mpeg'
Rewrite 4e50c358332cdb1a989ff88aa3361a175a47326c (2063/2098) (6485 seconds passed, remaining 110 predicted)    rm 'DATA/151205/20151205132024_1.mpeg'
Rewrite 661bf83edaf4815b88dc1e815ebd7e4509aff8d5 (2064/2098) (6488 seconds passed, remaining 106 predicted)    rm 'DATA/151205/20151205132024_1.mpeg'
Rewrite c55a58ecd3389ffc353c58815e681d6afdf5cfb7 (2065/2098) (6489 seconds passed, remaining 103 predicted)    rm 'DATA/151205/20151205132024_1.mpeg'
Rewrite e54f6f2dfac36563828575c53875840582637746 (2066/2098) (6491 seconds passed, remaining 100 predicted)    rm 'DATA/151205/20151205132024_1.mpeg'
Rewrite 02e20a9328284b658e43b31b905a8de6b271e19b (2067/2098) (6493 seconds passed, remaining 97 predicted)    rm 'DATA/151205/20151205132024_1.mpeg'
Rewrite 52943f3967932981a7a3f3e296fb6ebd8567d5b8 (2068/2098) (6495 seconds passed, remaining 94 predicted)    rm 'DATA/151205/20151205132024_1.mpeg'
Rewrite 48c1de8c0c30f3115aa80a1c82bdf3ad589280fb (2069/2098) (6496 seconds passed, remaining 91 predicted)    rm 'DATA/151205/20151205132024_1.mpeg'
Rewrite fcdcf918dfa5dae27ffe3263664cd790f04cad2e (2070/2098) (6498 seconds passed, remaining 87 predicted)    rm 'DATA/151205/20151205132024_1.mpeg'
Rewrite ce3759980539c0faf6769309ea2463b0e94320d0 (2071/2098) (6501 seconds passed, remaining 84 predicted)    rm 'DATA/151205/20151205132024_1.mpeg'
Rewrite d3ccc268289bcb5a79e204126cf42ed14031ec25 (2072/2098) (6503 seconds passed, remaining 81 predicted)    rm 'DATA/151205/20151205132024_1.mpeg'
Rewrite 6008b5c9ee5771156dbaac889e60536388ba34dd (2073/2098) (6505 seconds passed, remaining 78 predicted)    rm 'DATA/151205/20151205132024_1.mpeg'
Rewrite 7f9dd3109bc228cba0b7bbf430500aa5e470d39a (2074/2098) (6506 seconds passed, remaining 75 predicted)    rm 'DATA/151205/20151205132024_1.mpeg'
Rewrite 52ef1320e5f1bd71b97141ba6a4a6d2a3cf4716c (2075/2098) (6508 seconds passed, remaining 72 predicted)    rm 'DATA/151205/20151205132024_1.mpeg'
Rewrite 6928570b778c18d3eec45ceeb7bc308c4bd2e9a0 (2076/2098) (6510 seconds passed, remaining 68 predicted)    rm 'DATA/151205/20151205132024_1.mpeg'
Rewrite 2f8f04150059810abe68238851906e0bd0be093b (2077/2098) (6512 seconds passed, remaining 65 predicted)    rm 'DATA/151205/20151205132024_1.mpeg'
Rewrite 62175087c45ad4020bf8593625cd0265a141243d (2078/2098) (6514 seconds passed, remaining 62 predicted)    rm 'DATA/151205/20151205132024_1.mpeg'
Rewrite dab1ba1509d113e652b34ba242318e9d5ecadc86 (2098/2098) (6545 seconds passed, remaining 0 predicted)    
Ref 'refs/heads/master' was rewritten
Ref 'refs/remotes/origin/master' was rewritten

ついで,当該ファイルのすべての参照を消し,無効なreflogを消し,ガベージコレクションする.

$ git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
$ git reflog expire --expire=now --all
$ git gc --prune=now

ちなみに1度目には,この操作を実行しながら,自動コミットを止めず,このレポジトリの中で別の作業をしていたため,レポジトリの歴史改変と新規コミットが同時に行われることになった.何がどう悪かったのかはわからないが,結果として以下のようなメッセージが出て,履歴消去は成功しなかった.自動コミットを止め,作業ディレクトリをレポジトリ外に退避し,また何時間もかかる操作を再実行する羽目になった.

WARNING: Ref 'refs/heads/master' is unchanged
WARNING: Ref 'refs/remotes/origin/master' is unchanged

成功するとgit_find_big.shには消したファイルが出なくなる.

最終的に操作が完了したら,git push --all --force git push --tag --forceでリモートに改変した歴史を反映する.うまくいかなければリモートを作り直す

手順2

原理的には上記の方法で,対象とするファイルをひとつひとつ消していけばいい.金曜の夜に適当にシェルスクリプトを回して帰宅すれば,きっと月曜の朝には終わっているに違いない.

for i in `bash git_find_big.sh| awk '{print $4}'`
do
    git filter-branch --index-filter "git rm --ignore-unmatch $i" --tag-name-filter 'cat' -- --all
    git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
    git reflog expire --expire=now --all
    git gc --prune=now
done

だが,上記のQiitaでも紹介されているように,このくらいの単純な条件での消去であればもっと素早く終える方法がある.

rtyley.github.io

ダウンロードしたbfg-1.13.0.jarを実行する.実行場所はどこでもよく,ローカルを引数に取る.オプション--strip-blobs-bigger-thanにメガバイト単位で削除対象ファイルのサイズ下限を指定する.

$ java -jar /path/to/bfg-1.13.0.jar --strip-blobs-bigger-than 400M /path/to/repo/

Using repo : /Users/lingvisticae/repo/../repo/.git

Scanning packfile for large blobs: 16924
Scanning packfile for large blobs completed in 532 ms.
Found 15 blob ids for large blobs - biggest=8184205312 smallest=457097216
Total size (unpacked)=35647620530
Found 3184 objects to protect
Found 3 commit-pointing refs : HEAD, refs/heads/master, refs/remotes/origin/master

Protected commits
 -----------------

These are your protected commits, and so their contents will NOT be altered:

 * commit a017320a (protected by 'HEAD')

Cleaning
 --------

Found 2098 commits
Cleaning commits:       100% (2098/2098)
Cleaning commits completed in 441 ms.

Updating 2 Refs
 ---------------

	Ref                          Before     After   
	------------------------------------------------
	refs/heads/master          | a017320a | d765a52f
	refs/remotes/origin/master | ee062ba0 | b1563980

Updating references:    100% (2/2)
...Ref update completed in 14 ms.

Commit Tree-Dirt History
 ------------------------

	Earliest                                              Latest
	|                                                          |
	..........................................................DD

	D = dirty commits (file tree fixed)
	m = modified commits (commit message or parents changed)
	. = clean commits (no changes to file tree)

	                        Before     After   
	-------------------------------------------
	First modified commit | 46ac3103 | a1c7efed
	Last dirty commit     | 8257f140 | 289b5708

Deleted files
 -------------

	Filename                Git id             
	-------------------------------------------
	20151205132024_2.mpeg | f8029a99 (7.6 GB)  
	20151205132024_3.mpeg | 7adbbf4a (7.6 GB)  
	20151205132024_4.mpeg | cfb88c78 (435.9 MB)
	20151205193243.mpeg   | cbb79aa4 (6.6 GB)  
	C001_002_MIX.mp4      | b127d5f7 (661.9 MB)
	GP010006.MP4          | db8bb6f7 (3.7 GB)  
	T001_002_IC01.wav     | ee84c96c (543.9 MB)
	T001_002_IC02.wav     | ad898d79 (543.9 MB)
	T001_002_IC0A.wav     | a6b8e1b5 (543.9 MB)
	T001_002_MIX.mp4      | 4bb8a6c4 (2.0 GB)  
	T014_018_IC01.wav     | 3a2e8a8b (710.3 MB)
	T014_018_IC02.wav     | 225fd98a (710.3 MB)
	T014_018_IC0A.wav     | 03c97ee5 (710.3 MB)
	T014_018_MIX.mp4      | f4f1b570 (2.6 GB)  
	割り込み.mov              | 7c6004ef (809.1 MB)


In total, 72 object ids were changed. Full details are logged here:

	/Users/lingvisticae/repo/../repo.bfg-report/2019-10-17/21-32-11

BFG run is complete! When ready, run: git reflog expire --expire=now --all && git gc --prune=now --aggressive


 --
You can rewrite history in Git - don't let Trump do it for real!
Trump's administration has lied consistently, to make people give up on ever
being told the truth. Don't give up: https://www.theguardian.com/us-news/trump-administration
 --

ものの1秒もかからず終了した.

終了したら同様に参照の処理をする.

$ git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
$ git reflog expire --expire=now --all
$ git gc --prune=now

f:id:lingvisticae:20191018110418p:plain
縮小に成功したリモート

f:id:lingvisticae:20191018110520p:plain
縮小に成功したローカル