有的時(shí)候,你需要對倉庫進(jìn)行清理 - 使它的結(jié)構(gòu)變得更緊湊,或是對導(dǎo)入的倉庫進(jìn)行清理,或是恢復(fù)丟失的內(nèi)容。 這個(gè)小節(jié)將會介紹這些情況中的一部分。
Git 會不定時(shí)地自動運(yùn)行一個(gè)叫做 “auto gc” 的命令。 大多數(shù)時(shí)候,這個(gè)命令并不會產(chǎn)生效果。 然而,如果有太多松散對象(不在包文件中的對象)或者太多包文件,Git 會運(yùn)行一個(gè)完整的?git gc
?命令。 “gc” 代表垃圾回收,這個(gè)命令會做以下事情:收集所有松散對象并將它們放置到包文件中,將多個(gè)包文件合并為一個(gè)大的包文件,移除與任何提交都不相關(guān)的陳舊對象。
可以像下面一樣手動執(zhí)行自動垃圾回收:
$ git gc --auto
就像上面提到的,這個(gè)命令通常并不會產(chǎn)生效果。 大約需要 7000 個(gè)以上的松散對象或超過 50 個(gè)的包文件才能讓 Git 啟動一次真正的 gc 命令。 你可以通過修改?gc.auto
?與gc.autopacklimit
?的設(shè)置來改動這些數(shù)值。
gc
?將會做的另一件事是打包你的引用到一個(gè)單獨(dú)的文件。 假設(shè)你的倉庫包含以下分支與標(biāo)簽:
$ find .git/refs -type f
.git/refs/heads/experiment
.git/refs/heads/master
.git/refs/tags/v1.0
.git/refs/tags/v1.1
如果你執(zhí)行了?git gc
?命令,refs
?目錄中將不會再有這些文件。 為了保證效率 Git 會將它們移動到名為?.git/packed-refs
?的文件中,就像這樣:
$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled
cac0cab538b970a37ea1e769cbbde608743bc96d refs/heads/experiment
ab1afef80fac8e34258ff41fc1b867c702daa24b refs/heads/master
cac0cab538b970a37ea1e769cbbde608743bc96d refs/tags/v1.0
9585191f37f7b0fb9444f35a9bf50de191beadc2 refs/tags/v1.1
^1a410efbd13591db07496601ebc7a059dd55cfe9
如果你更新了引用,Git 并不會修改這個(gè)文件,而是向?refs/heads
?創(chuàng)建一個(gè)新的文件。 為了獲得指定引用的正確 SHA-1 值,Git 會首先在?refs
?目錄中查找指定的引用,然后再到?packed-refs
?文件中查找。 所以,如果你在?refs
?目錄中找不到一個(gè)引用,那么它或許在?packed-refs
?文件中。
注意這個(gè)文件的最后一行,它會以?^
?開頭。 這個(gè)符號表示它上一行的標(biāo)簽是附注標(biāo)簽,那一行是附注標(biāo)簽指向的那個(gè)提交。
在你使用 Git 的時(shí)候,你可能會意外丟失一次提交。 通常這是因?yàn)槟銖?qiáng)制刪除了正在工作的分支,但是最后卻發(fā)現(xiàn)你還需要這個(gè)分支;亦或者硬重置了一個(gè)分支,放棄了你想要的提交。 如果這些事情已經(jīng)發(fā)生,該如何找回你的提交呢?
下面的例子將硬重置你的測試倉庫中的 master 分支到一個(gè)舊的提交,以此來恢復(fù)丟失的提交。 首先,讓我們看看你的倉庫現(xiàn)在在什么地方:
$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
現(xiàn)在,我們將?master
?分支硬重置到第三次提交:
$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
HEAD is now at 1a410ef third commit
$ git log --pretty=oneline
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
現(xiàn)在頂部的兩個(gè)提交已經(jīng)丟失了 - 沒有分支指向這些提交。 你需要找出最后一次提交的 SHA-1 然后增加一個(gè)指向它的分支。 竅門就是找到最后一次的提交的 SHA-1 - 但是估計(jì)你記不起來了,對嗎?
最方便,也是最常用的方法,是使用一個(gè)名叫?git reflog
?的工具。 當(dāng)你正在工作時(shí),Git 會默默地記錄每一次你改變 HEAD 時(shí)它的值。 每一次你提交或改變分支,引用日志都會被更新。 引用日志(reflog)也可以通過?git update-ref
?命令更新,我們在?Git 引用?有提到使用這個(gè)命令而不是是直接將 SHA-1 的值寫入引用文件中的原因。 你可以在任何時(shí)候通過執(zhí)行?git reflog
?命令來了解你曾經(jīng)做過什么:
$ git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: modified repo.rb a bit
484a592 HEAD@{2}: commit: added repo.rb
這里可以看到我們已經(jīng)檢出的兩次提交,然而并沒有足夠多的信息。 為了使顯示的信息更加有用,我們可以執(zhí)行?git log -g
,這個(gè)命令會以標(biāo)準(zhǔn)日志的格式輸出引用日志。
$ git log -g
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Reflog: HEAD@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:22:37 2009 -0700
third commit
commit ab1afef80fac8e34258ff41fc1b867c702daa24b
Reflog: HEAD@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:15:24 2009 -0700
modified repo.rb a bit
看起來下面的那個(gè)就是你丟失的提交,你可以通過創(chuàng)建一個(gè)新的分支指向這個(gè)提交來恢復(fù)它。 例如,你可以創(chuàng)建一個(gè)名為?recover-branch
?的分支指向這個(gè)提交(ab1afef):
$ git branch recover-branch ab1afef
$ git log --pretty=oneline recover-branch
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
不錯,現(xiàn)在有一個(gè)名為?recover-branch
?的分支是你的?master
?分支曾經(jīng)指向的地方,再一次使得前兩次提交可到達(dá)了。 接下來,假設(shè)你丟失的提交因?yàn)槟承┰虿辉谝萌罩局?- 我們可以通過移除?recover-branch
?分支并刪除引用日志來模擬這種情況。 現(xiàn)在前兩次提交又不被任何分支指向了:
$ git branch -D recover-branch
$ rm -Rf .git/logs/
由于引用日志數(shù)據(jù)存放在?.git/logs/
?目錄中,現(xiàn)在你已經(jīng)沒有引用日志了。 這時(shí)該如何恢復(fù)那次提交? 一種方式是使用?git fsck
?實(shí)用工具,將會檢查數(shù)據(jù)庫的完整性。 如果使用一個(gè)?--full
?選項(xiàng)運(yùn)行它,它會向你顯示出所有沒有被其他對象指向的對象:
$ git fsck --full
Checking object directories: 100% (256/256), done.
Checking objects: 100% (18/18), done.
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293
在這個(gè)例子中,你可以在 “dangling commit” 后看到你丟失的提交。 現(xiàn)在你可以用和之前相同的方法恢復(fù)這個(gè)提交,也就是添加一個(gè)指向這個(gè)提交的分支。
Git 有很多很棒的功能,但是其中一個(gè)特性會導(dǎo)致問題,git clone
?會下載整個(gè)項(xiàng)目的歷史,包括每一個(gè)文件的每一個(gè)版本。 如果所有的東西都是源代碼那么這很好,因?yàn)?Git 被高度優(yōu)化來有效地存儲這種數(shù)據(jù)。 然而,如果某個(gè)人在之前向項(xiàng)目添加了一個(gè)大小特別大的文件,即使你將這個(gè)文件從項(xiàng)目中移除了,每次克隆還是都要強(qiáng)制的下載這個(gè)大文件。 之所以會產(chǎn)生這個(gè)問題,是因?yàn)檫@個(gè)文件在歷史中是存在的,它會永遠(yuǎn)在那里。
當(dāng)你遷移 Subversion 或 Perforce 倉庫到 Git 的時(shí)候,這會是一個(gè)嚴(yán)重的問題。 因?yàn)檫@些版本控制系統(tǒng)并不下載所有的歷史文件,所以這種文件所帶來的問題比較少。 如果你從其他的版本控制系統(tǒng)遷移到 Git 時(shí)發(fā)現(xiàn)倉庫比預(yù)期的大得多,那么你就需要找到并移除這些大文件。
警告:這個(gè)操作對提交歷史的修改是破壞性的。?它會從你必須修改或移除一個(gè)大文件引用最早的樹對象開始重寫每一次提交。 如果你在導(dǎo)入倉庫后,在任何人開始基于這些提交工作前執(zhí)行這個(gè)操作,那么將不會有任何問題 - 否則,你必須通知所有的貢獻(xiàn)者他們需要將他們的成果變基到你的新提交上。
為了演示,我們將添加一個(gè)大文件到測試倉庫中,并在下一次提交中刪除它,現(xiàn)在我們需要找到它,并將它從倉庫中永久刪除。 首先,添加一個(gè)大文件到倉庫中:
$ curl https://www.kernel.org/pub/software/scm/git/git-2.1.0.tar.gz > git.tgz
$ git add git.tgz
$ git commit -m 'add git tarball'
[master 7b30847] add git tarball
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 git.tgz
哎呀 - 其實(shí)這個(gè)項(xiàng)目并不需要這個(gè)巨大的壓縮文件。 現(xiàn)在我們將它移除:
$ git rm git.tgz
rm 'git.tgz'
$ git commit -m 'oops - removed large tarball'
[master dadf725] oops - removed large tarball
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 git.tgz
現(xiàn)在,我們執(zhí)行?gc
?來查看數(shù)據(jù)庫占用了多少空間:
$ git gc
Counting objects: 17, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (13/13), done.
Writing objects: 100% (17/17), done.
Total 17 (delta 1), reused 10 (delta 0)
你也可以執(zhí)行?count-objects
?命令來快速的查看占用空間大小:
$ git count-objects -v
count: 7
size: 32
in-pack: 17
packs: 1
size-pack: 4868
prune-packable: 0
garbage: 0
size-garbage: 0
size-pack
?的數(shù)值指的是你的包文件以 KB 為單位計(jì)算的大小,所以你大約占用了 5MB 的空間。 在最后一次提交前,使用了不到 2KB - 顯然,從之前的提交中移除文件并不能從歷史中移除它。 每一次有人克隆這個(gè)倉庫時(shí),他們將必須克隆所有的 5MB 來獲得這個(gè)微型項(xiàng)目,只因?yàn)槟阋馔獾靥砑恿艘粋€(gè)大文件。 現(xiàn)在來讓我們徹底的移除這個(gè)文件。
首先你必須找到它。 在本例中,你已經(jīng)知道是哪個(gè)文件了。 但是假設(shè)你不知道;該如何找出哪個(gè)文件或哪些文件占用了如此多的空間? 如果你執(zhí)行?git gc
?命令,所有的對象將被放入一個(gè)包文件中,你可以通過運(yùn)行?git verify-pack
?命令,然后對輸出內(nèi)容的第三列(即文件大?。┻M(jìn)行排序,從而找出這個(gè)大文件。 你也可以將這個(gè)命令的執(zhí)行結(jié)果通過管道傳送給?tail
?命令,因?yàn)槟阒恍枰业搅性谧詈蟮膸讉€(gè)大對象。
$ git verify-pack -v .git/objects/pack/pack-29…69.idx \
| sort -k 3 -n \
| tail -3
dadf7258d699da2c8d89b09ef6670edb7d5f91b4 commit 229 159 12
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob 22044 5792 4977696
82c99a3e86bb1267b236a4b6eff7868d97489af1 blob 4975916 4976258 1438
你可以看到這個(gè)大對象出現(xiàn)在返回結(jié)果的最底部:占用 5MB 空間。 為了找出具體是哪個(gè)文件,可以使用?rev-list
?命令,我們在?指定特殊的提交信息格式?中曾提到過。 如果你傳遞?--objects
?參數(shù)給?rev-list
?命令,它就會列出所有提交的 SHA-1、數(shù)據(jù)對象的 SHA-1 和與它們相關(guān)聯(lián)的文件路徑。 可以使用以下命令來找出你的數(shù)據(jù)對象的名字:
$ git rev-list --objects --all | grep 82c99a3
82c99a3e86bb1267b236a4b6eff7868d97489af1 git.tgz
現(xiàn)在,你只需要從過去所有的樹中移除這個(gè)文件。 使用以下命令可以輕松地查看哪些提交對這個(gè)文件產(chǎn)生改動:
$ git log --oneline --branches -- git.tgz
dadf725 oops - removed large tarball
7b30847 add git tarball
現(xiàn)在,你必須重寫?7b30847
?提交之后的所有提交來從 Git 歷史中完全移除這個(gè)文件。 為了執(zhí)行這個(gè)操作,我們要使用?filter-branch
?命令,這個(gè)命令在?重寫歷史?中也使用過:
$ git filter-branch --index-filter \
'git rm --ignore-unmatch --cached git.tgz' -- 7b30847^..
Rewrite 7b30847d080183a1ab7d18fb202473b3096e9f34 (1/2)rm 'git.tgz'
Rewrite dadf7258d699da2c8d89b09ef6670edb7d5f91b4 (2/2)
Ref 'refs/heads/master' was rewritten
--index-filter
?選項(xiàng)類似于在?重寫歷史?中提到的的?--tree-filter
?選項(xiàng),不過這個(gè)選項(xiàng)并不會讓命令將修改在硬盤上檢出的文件,而只是修改在暫存區(qū)或索引中的文件。
你必須使用?git rm --cached
?命令來移除文件,而不是通過類似?rm file
?的命令 - 因?yàn)槟阈枰獜乃饕幸瞥?,而不是磁盤中。 還有一個(gè)原因是速度 - Git 在運(yùn)行過濾器時(shí),并不會檢出每個(gè)修訂版本到磁盤中,所以這個(gè)過程會非常快。 如果愿意的話,你也可以通過?--tree-filter
?選項(xiàng)來完成同樣的任務(wù)。?git rm
?命令的?--ignore-unmatch
?選項(xiàng)告訴命令:如果嘗試刪除的模式不存在時(shí),不提示錯誤。 最后,使用?filter-branch
?選項(xiàng)來重寫自?7b30847
?提交以來的歷史,也就是這個(gè)問題產(chǎn)生的地方。 否則,這個(gè)命令會從最舊的提交開始,這將會花費(fèi)許多不必要的時(shí)間。
你的歷史中將不再包含對那個(gè)文件的引用。 不過,你的引用日志和你在?.git/refs/original
?通過?filter-branch
?選項(xiàng)添加的新引用中還存有對這個(gè)文件的引用,所以你必須移除它們?nèi)缓笾匦麓虬鼣?shù)據(jù)庫。 在重新打包前需要移除任何包含指向那些舊提交的指針的文件:
$ rm -Rf .git/refs/original
$ rm -Rf .git/logs/
$ git gc
Counting objects: 15, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (11/11), done.
Writing objects: 100% (15/15), done.
Total 15 (delta 1), reused 12 (delta 0)
讓我們看看你省了多少空間。
$ git count-objects -v
count: 11
size: 4904
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0
打包的倉庫大小下降到了 8K,比 5MB 好很多。 可以從 size 的值看出,這個(gè)大文件還在你的松散對象中,并沒有消失;但是它不會在推送或接下來的克隆中出現(xiàn),這才是最重要的。 如果真的想要刪除它,可以通過有?--expire
?選項(xiàng)的?git prune
?命令來完全地移除那個(gè)對象:
$ git prune --expire now
$ git count-objects -v
count: 0
size: 0
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0
更多建議: