偶然發(fā)現(xiàn)Git文件夾非常大,使用BGF來處理Git歷史Blob文件
我們使用Git來管理項目的時候,可能會提交一些Blob的二進(jìn)制文件,這些文件并不能像文本文件一樣采用diff delta的形式進(jìn)行版本控制。如果這些文件一直跟隨master的主版本,那么就是屬于有效的文件。
然而很多時候這些二進(jìn)制文件會被刪除重建,那么由于Git的特性,這些文件會一直留在Git的歷史記錄中,這樣會導(dǎo)致Git倉庫變得龐大,不利于版本控制和遷移。最直觀的就是clone的時候會很慢,而使用--depth=1則無法看到歷史提交的代碼。
查找歷史文件
歷史提交的二進(jìn)制文件通常我們可以認(rèn)為是不需要的,然而在多人協(xié)作的時候這個事情我們并不能非常確定。因此我們需要主動查找較大的二進(jìn)制文件來處理,最簡單的辦法就是直接掃描大文件。
git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.idx | sort -k 3 -n | tail -10 | awk '{print$1}')"
通過這個命令我們可以看到歷史提交中最大的10個文件,然后我們可以根據(jù)這個文件的hash值來查找這個文件的提交記錄。如果需要看到文件的大小,也可以再補(bǔ)充下輸出。
git verify-pack -v .git/objects/pack/*.idx \
| grep blob \
| sort -k 3 -n \
| tail -10 \
| awk '{print $1, $3}' \
| while read -r hash size; do
file=$(git rev-list --objects --all | grep "$hash")
echo "$file $size"
done
BFG Repo-Cleaner
雖然可以使用git-filter-branch來處理歷史提交,但是這個命令的效率比較低,且容易出現(xiàn)錯誤。BFG是一種更簡單、更快捷的替代方法,主要是用來處理歷史提交的文件,可以刪除指定的文件,也可以替換文件內(nèi)容。
BFG需要使用Java來運行,因此需要先確保運行環(huán)境JDK >= 8。之后使用git clone --mirror來克隆倉庫,然后使用BFG來處理歷史提交。
git clone --mirror git://example.com/some-big-repo.git
需要注意的是,使用--mirror標(biāo)識會將整個倉庫的完整副本克隆下來,包括所有的提交歷史,并且不會存在任何普通文件。同樣的,當(dāng)處理完成之后這個倉庫所有的分支都會被修改,因此需要謹(jǐn)慎操作。
而如果僅處理單個分支,那就是使用常規(guī)的git clone即可。但是需要注意的是,由于其他分支仍然有可能持有舊的分支內(nèi)容,因此就必須要將所有涉及到的分支依次處理,這樣就非常麻煩了。
接下來就是使用BFG來處理歷史提交,BFG的使用非常簡單,只需要指定需要處理的文件即可。下載BFG的jar包可以在https://rtyley.github.io/bfg-repo-cleaner/中找到。
java -jar bfg.jar --delete-files path/to/file.txt some-big-repo.git
BFG還提供了一些其他的功能,比如刪除指定大小的文件、替換文件內(nèi)容、刪除文件夾、刪除文件通用匹配符。
java -jar bfg.jar --strip-blobs-bigger-than 1M some-big-repo.git
java -jar bfg.jar --replace-text pwd.txt some-big-repo.git
java -jar bfg.jar --delete-folders path some-big-repo.git
java -jar bfg.jar --delete-files '*.png' some-big-repo.git
但是這里的匹配模式也存在局限,例如無法同時指定文件和大小,例如需要移除> 1M的png文件是做不到的,經(jīng)過測試其匹配模式總是傾向于后設(shè)置的模式。不過這里并沒有閱讀源碼,只是簡單的測試判斷。
此外,不需要擔(dān)心BFG會刪除HEAD的提交,BFG不會處理HEAD的提交,即使BFG會從早期的歷史記錄中刪除文件,當(dāng)然也可以通過--no-blob-protection來關(guān)閉保護(hù)。
WARNING: The dirty content above may be removed from other commits, but as
the *protected* commits still use it, it will STILL exist in your repository.
在處理完成后,在同級目錄下會生成report文件夾,其中包含了處理的文件信息,可以查看處理的文件數(shù)量、大小等信息,以及HASH變更的信息。在檢查無誤后,要將歷史記錄的過期時間設(shè)置為現(xiàn)在,且需要使用git來清理無引用的數(shù)據(jù),這樣就可以將BFG處理的數(shù)據(jù)真正刪除。
cd some-big-repo.git
git reflog expire --expire=now --all && git gc --prune=now --aggressive
此時就可以通過du命令檢查文件夾的大小了,通常我們只需要關(guān)注.git文件夾即可。
du -sh .git
最后,直接使用git push來推送到遠(yuǎn)程倉庫即可,這樣就完成了歷史提交的處理。注意如果使用分支模式的話,就需要加入-force選項來強(qiáng)制推送。
git push
這里還需要有一個額外的步驟,需要讓每個參與者將本地的倉庫刪除,然后完整重新clone最新的倉庫,防止持有舊數(shù)據(jù)的倉庫重新推回倉庫。此外,也可以使用git-filter-repo來實現(xiàn)類似的處理。
GitHub
從歷史記錄中刪除文件并不是簡單的事情,如果需要我們手動來執(zhí)行操作的話,就很像我們從某一次提交開始,不斷向后rebase。那么在這個過程中自然就會導(dǎo)致commit的hash值發(fā)生變化,從而出現(xiàn)一些問題,這里我們主要關(guān)注在GitHub的表現(xiàn)。
- 如果二進(jìn)制文件是在很久前的提交,例如
5年前的提交,而假設(shè)我們僅會刪除此提交的某個文件,對于其他的提交并沒有處理。但是由于需要重寫歷史提交記錄,這就會導(dǎo)致從5年前引入的id開始到最新的提交全部被重寫,這部分可以在BFG的Commit Tree-Dirt History中關(guān)注到。 - 前邊我們也提到了
BFG會將hash變化信息寫入report文件夾,實際上在重寫的commit描述信息中,也會發(fā)現(xiàn)Former-commit-id: xxx的數(shù)據(jù),用以標(biāo)識這個commit重寫前的引用。 - 在
GitHub的contributions面板中,也就是綠色的瓷磚部分,會出現(xiàn)重寫commit的歷史提交出現(xiàn)重復(fù)貢獻(xiàn)的現(xiàn)象,也就是說原來可能僅有1個提交,現(xiàn)在變成了2個,對于這個問題是可以減小影響面的。 - 雖然
contributions面板中會出現(xiàn)重復(fù)的提交,但是通過api獲取的提交記錄總數(shù)中并不會出現(xiàn)重復(fù)的數(shù)量增量,也就是說GitHub并沒有將重寫的commit計入歷史提交記錄中。 - 對于通過分支模式而不是
mirror模式清理的單獨分支,雖然通過BFG可以將歷史提交的二進(jìn)制文件刪除,但是其commit數(shù)量的計算會出現(xiàn)問題,其會切斷fork之前的聯(lián)系,也就是說原本fork分支的提交記錄會被重新計算。 - 雖然對于主分支的提交數(shù)量不會影響,但是此時如果我們打開重寫過后的
commit描述,可以發(fā)現(xiàn)仍然可以找到原本的commit。這就意味著這個object實際上并沒有被刪除,只是不再被引用,而是處于游離指針的狀態(tài)。
實際上這里的影響面還是挺大的,特別是對于早些時間引入的二進(jìn)制文件,會導(dǎo)致大范圍的歷史提交記錄重寫。
對于contributions面板的影響,也是看起來比較大的問題,不過我們可以先fork主分支,然后再將主分支設(shè)置為fork分支,然后再更改名稱的形式來解決這個問題。但是這個方式并沒有完全解決問題,還是會因為提交日期的重寫而導(dǎo)致contributions面板的數(shù)據(jù)不準(zhǔn)確,不過影響面小了很多。
而對于其他問題,通常都是無法避免的問題,因為Git的特性決定了這個問題的存在。如果需要處理歷史提交中私有文件的泄漏,則通常認(rèn)為是不可靠的,此時必須要立即修正密鑰。因此無論是當(dāng)前提交還是在處理歷史提交的時候,需要謹(jǐn)慎操作,不要泄漏私密文件,盡量避免對歷史提交的處理。

浙公網(wǎng)安備 33010602011771號