你已經(jīng)掌握了 Git 的基本用法柑土,只消熟練使用幾個(gè)常用命令,足以應(yīng)付開發(fā)過(guò)程中的絕大多數(shù)場(chǎng)景酌予。在 Git 的幫助下磺箕,你過(guò)上了快樂(lè)的生活。然而抛虫,某天早上你一覺(jué)醒來(lái)之后松靡,發(fā)現(xiàn)了一件令人納悶的事情:“為什么我的 Git 倉(cāng)庫(kù)變得如此臃腫?”
情形一:倉(cāng)庫(kù)自身的增長(zhǎng)
大多數(shù)版本控制系統(tǒng)存儲(chǔ)的是一組初始文件建椰,以及每個(gè)文件隨著時(shí)間的演進(jìn)而逐步積累起來(lái)的差異雕欺;而 Git 則會(huì)把文件的每一個(gè)差異化版本都記錄在案(關(guān)于 Git 是如何存儲(chǔ)數(shù)據(jù)的,請(qǐng)參閱這篇文章)棉姐。這意味著屠列,即使你只改動(dòng)了某個(gè)文件的一行內(nèi)容,Git 也會(huì)生成一個(gè)全新的對(duì)象來(lái)存儲(chǔ)新的文件內(nèi)容伞矩。
如果你改動(dòng)了一個(gè)很大的文件笛洛,問(wèn)題就來(lái)了。
對(duì)象碎片
現(xiàn)在乃坤,讓我們生成一個(gè)包含 1000000 行隨機(jī)字符串的大文本文件苛让,并把它添加到版本庫(kù)中:
$ perl -le 'for (1..1000000) { print map { (0..9, "a".."z")[rand 36] } 1..80 }' > bigfile
$ git add bigfile
我們看到沟蔑,Git 已經(jīng)為這個(gè)文件生成了一個(gè) Blob 對(duì)象,大小是 54M狱杰。
$ find .git/objects -type f
.git/objects/7f/a055b2d22855b67287e4e30d9a91584c8b27c1
$
$ du -ah # 此處略去了無(wú)關(guān)輸出
54M ./.git/objects/7f/a055b2d22855b67287e4e30d9a91584c8b27c1
77M ./bigfile
132M .
如果往文件末尾添加一行瘦材,會(huì)怎么樣呢?
$ perl -le 'print map { (0..9, "a".."z")[rand 36] } 1..80' >> bigfile
$ git add bigfile
可以看到仿畸,Git 生成了一個(gè)全新的 Blog 對(duì)象來(lái)存儲(chǔ)新的文件內(nèi)容宇色,這個(gè)對(duì)象的大小同樣是 54M。倉(cāng)庫(kù)瞬間患上了肥胖癥颁湖。
$ find .git/objects -type f
.git/objects/7f/a055b2d22855b67287e4e30d9a91584c8b27c1
.git/objects/f3/79c10af3f3e497d37558ac2497fe3c69d2de89
$
$ du -ah # 此處略去了無(wú)關(guān)輸出
54M ./.git/objects/7f/a055b2d22855b67287e4e30d9a91584c8b27c1
54M ./.git/objects/f3/79c10af3f3e497d37558ac2497fe3c69d2de89
77M ./bigfile
186M .
你的倉(cāng)庫(kù)里面現(xiàn)在有兩個(gè)內(nèi)容幾乎完全相同,大小均為 54M 的龐大對(duì)象例隆。如果 Git 可以只保存其中一個(gè)對(duì)象甥捺,再保存另一個(gè)對(duì)象與這個(gè)對(duì)象的差異內(nèi)容,豈不妙哉镀层?
gc 命令
“垃圾回收”是一個(gè)很親切的功能镰禾。讓我們開始吧:
$ git gc --prune=now
現(xiàn)在,重新檢視一下倉(cāng)庫(kù)的大小唱逢,發(fā)現(xiàn)確實(shí)有效拔庹臁:
$ find .git/objects -type f
.git/objects/info/packs
.git/objects/pack/pack-9d75315485cb7bfbf51ce5c94a4535da99b58dbb.idx
.git/objects/pack/pack-9d75315485cb7bfbf51ce5c94a4535da99b58dbb.pack
$
$ du -ah # 此處略去了無(wú)關(guān)輸出
4.0K ./.git/objects/pack/pack-9d75315485cb7bfbf51ce5c94a4535da99b58dbb.idx
52M ./.git/objects/pack/pack-9d75315485cb7bfbf51ce5c94a4535da99b58dbb.pack
77M ./bigfile
130M .
運(yùn)行 gc
命令之后,兩個(gè) Blob 對(duì)象不見(jiàn)了坞古。Git 創(chuàng)建了一個(gè)包文件和一個(gè)索引文件备韧。包文件中包含了之前的兩個(gè) Blob 對(duì)象,索引文件中包含了每個(gè)對(duì)象在包文件中的偏移信息痪枫。Git 在打包的過(guò)程中使用了增量編碼方案(delta encoding)织堂,只保存對(duì)象的不同版本之間的差異,這使得倉(cāng)庫(kù)瘦身成功奶陈。
不過(guò)...
實(shí)際上易阳,你并不需要手動(dòng)調(diào)用 gc
命令。每當(dāng)碎片對(duì)象過(guò)多吃粒,或者你向遠(yuǎn)端服務(wù)器發(fā)起推送的時(shí)候潦俺,Git 就會(huì)自動(dòng)執(zhí)行一次打包過(guò)程。
情形二:錯(cuò)誤的大文件
讓我們添加一個(gè)名為 new.txt
的新文件徐勃,并且執(zhí)行兩次提交:
$ git commit -m "first commit"
$ echo 'new file' > new.txt
$ git commit -a "second commit"
當(dāng)執(zhí)行第二次提交的時(shí)候事示,你突然發(fā)現(xiàn),其實(shí) bigfile
這個(gè)文件在項(xiàng)目中并沒(méi)什么卵用僻肖。然而它很大很魂。即使你把這個(gè)文件從項(xiàng)目中移除了,它還是會(huì)頑固地永遠(yuǎn)存在于你的提交歷史中檐涝。有沒(méi)有辦法把這個(gè)文件從歷次提交中徹底地移除呢遏匆?
辦法是有的法挨,不過(guò)務(wù)必要謹(jǐn)慎哦。
能夠勝任這個(gè)任務(wù)的命令叫做 filter-branch:
$ git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch bigfile' \
--prune-empty --tag-name-filter cat -- --all
Rewrite 1d92bc51b15c80582cef9cfb27ee056f000590bc (1/2)rm 'bigfile'
Rewrite 4ef010df40a1e81b1f9a11391d63879b649e9690 (2/2)rm 'bigfile'
Ref 'refs/heads/master' was rewritten
然后幅聘,刪除緩存的對(duì)象凡纳。這一步可以暫時(shí)跳過(guò),等到確認(rèn)完全不會(huì)出現(xiàn)問(wèn)題之后再執(zhí)行(可以說(shuō)帝蒿,這些緩存對(duì)象給你提供了撤銷操作的最后一次機(jī)會(huì))荐糜。
$ git for-each-ref --format='delete %(refname)' refs/original | git update-ref --stdin
$ git reflog expire --expire=now --all
$ git gc --prune=now
現(xiàn)在倉(cāng)庫(kù)的總大小只有 88K 了,是不是很棒:
$ du -ah # 此處略去了無(wú)關(guān)輸出
4.0K ./.git/objects/pack/pack-846da79290b3ef2f6617aa8aab03e4f54439a40a.idx
4.0K ./.git/objects/pack/pack-846da79290b3ef2f6617aa8aab03e4f54439a40a.pack
4.0K ./new.txt
88K .
當(dāng)然葛超,你可能還需要把這一次的改動(dòng)提交到遠(yuǎn)端倉(cāng)庫(kù):
$ git push --force --verbose --dry-run
$ git push --force