10.7 維護與數(shù)據(jù)恢復

2018-02-24 15:22 更新

維護與數(shù)據(jù)恢復

有的時候,你需要對倉庫進行清理 - 使它的結(jié)構(gòu)變得更緊湊,或是對導入的倉庫進行清理,或是恢復丟失的內(nèi)容。 這個小節(jié)將會介紹這些情況中的一部分。

維護

Git 會不定時地自動運行一個叫做 “auto gc” 的命令。 大多數(shù)時候,這個命令并不會產(chǎn)生效果。 然而,如果有太多松散對象(不在包文件中的對象)或者太多包文件,Git 會運行一個完整的?git gc?命令。 “gc” 代表垃圾回收,這個命令會做以下事情:收集所有松散對象并將它們放置到包文件中,將多個包文件合并為一個大的包文件,移除與任何提交都不相關(guān)的陳舊對象。

可以像下面一樣手動執(zhí)行自動垃圾回收:

$ git gc --auto

就像上面提到的,這個命令通常并不會產(chǎn)生效果。 大約需要 7000 個以上的松散對象或超過 50 個的包文件才能讓 Git 啟動一次真正的 gc 命令。 你可以通過修改?gc.auto?與gc.autopacklimit?的設(shè)置來改動這些數(shù)值。

gc?將會做的另一件事是打包你的引用到一個單獨的文件。 假設(shè)你的倉庫包含以下分支與標簽:

$ 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 并不會修改這個文件,而是向?refs/heads?創(chuàng)建一個新的文件。 為了獲得指定引用的正確 SHA-1 值,Git 會首先在?refs?目錄中查找指定的引用,然后再到?packed-refs?文件中查找。 所以,如果你在?refs?目錄中找不到一個引用,那么它或許在?packed-refs?文件中。

注意這個文件的最后一行,它會以?^?開頭。 這個符號表示它上一行的標簽是附注標簽,那一行是附注標簽指向的那個提交。

數(shù)據(jù)恢復

在你使用 Git 的時候,你可能會意外丟失一次提交。 通常這是因為你強制刪除了正在工作的分支,但是最后卻發(fā)現(xiàn)你還需要這個分支;亦或者硬重置了一個分支,放棄了你想要的提交。 如果這些事情已經(jīng)發(fā)生,該如何找回你的提交呢?

下面的例子將硬重置你的測試倉庫中的 master 分支到一個舊的提交,以此來恢復丟失的提交。 首先,讓我們看看你的倉庫現(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)在頂部的兩個提交已經(jīng)丟失了 - 沒有分支指向這些提交。 你需要找出最后一次提交的 SHA-1 然后增加一個指向它的分支。 竅門就是找到最后一次的提交的 SHA-1 - 但是估計你記不起來了,對嗎?

最方便,也是最常用的方法,是使用一個名叫?git reflog?的工具。 當你正在工作時,Git 會默默地記錄每一次你改變 HEAD 時它的值。 每一次你提交或改變分支,引用日志都會被更新。 引用日志(reflog)也可以通過?git update-ref?命令更新,我們在?Git 引用?有提到使用這個命令而不是是直接將 SHA-1 的值寫入引用文件中的原因。 你可以在任何時候通過執(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,這個命令會以標準日志的格式輸出引用日志。

$ 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

看起來下面的那個就是你丟失的提交,你可以通過創(chuàng)建一個新的分支指向這個提交來恢復它。 例如,你可以創(chuàng)建一個名為?recover-branch?的分支指向這個提交(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)在有一個名為?recover-branch?的分支是你的?master?分支曾經(jīng)指向的地方,再一次使得前兩次提交可到達了。 接下來,假設(shè)你丟失的提交因為某些原因不在引用日志中 - 我們可以通過移除?recover-branch?分支并刪除引用日志來模擬這種情況。 現(xiàn)在前兩次提交又不被任何分支指向了:

$ git branch -D recover-branch
$ rm -Rf .git/logs/

由于引用日志數(shù)據(jù)存放在?.git/logs/?目錄中,現(xiàn)在你已經(jīng)沒有引用日志了。 這時該如何恢復那次提交? 一種方式是使用?git fsck?實用工具,將會檢查數(shù)據(jù)庫的完整性。 如果使用一個?--full?選項運行它,它會向你顯示出所有沒有被其他對象指向的對象:

$ 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

在這個例子中,你可以在 “dangling commit” 后看到你丟失的提交。 現(xiàn)在你可以用和之前相同的方法恢復這個提交,也就是添加一個指向這個提交的分支。

移除對象

Git 有很多很棒的功能,但是其中一個特性會導致問題,git clone?會下載整個項目的歷史,包括每一個文件的每一個版本。 如果所有的東西都是源代碼那么這很好,因為 Git 被高度優(yōu)化來有效地存儲這種數(shù)據(jù)。 然而,如果某個人在之前向項目添加了一個大小特別大的文件,即使你將這個文件從項目中移除了,每次克隆還是都要強制的下載這個大文件。 之所以會產(chǎn)生這個問題,是因為這個文件在歷史中是存在的,它會永遠在那里。

當你遷移 Subversion 或 Perforce 倉庫到 Git 的時候,這會是一個嚴重的問題。 因為這些版本控制系統(tǒng)并不下載所有的歷史文件,所以這種文件所帶來的問題比較少。 如果你從其他的版本控制系統(tǒng)遷移到 Git 時發(fā)現(xiàn)倉庫比預期的大得多,那么你就需要找到并移除這些大文件。

警告:這個操作對提交歷史的修改是破壞性的。?它會從你必須修改或移除一個大文件引用最早的樹對象開始重寫每一次提交。 如果你在導入倉庫后,在任何人開始基于這些提交工作前執(zhí)行這個操作,那么將不會有任何問題 - 否則,你必須通知所有的貢獻者他們需要將他們的成果變基到你的新提交上。

為了演示,我們將添加一個大文件到測試倉庫中,并在下一次提交中刪除它,現(xiàn)在我們需要找到它,并將它從倉庫中永久刪除。 首先,添加一個大文件到倉庫中:

$ 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

哎呀 - 其實這個項目并不需要這個巨大的壓縮文件。 現(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?命令來快速的查看占用空間大?。?/p>

$ 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 為單位計算的大小,所以你大約占用了 5MB 的空間。 在最后一次提交前,使用了不到 2KB - 顯然,從之前的提交中移除文件并不能從歷史中移除它。 每一次有人克隆這個倉庫時,他們將必須克隆所有的 5MB 來獲得這個微型項目,只因為你意外地添加了一個大文件。 現(xiàn)在來讓我們徹底的移除這個文件。

首先你必須找到它。 在本例中,你已經(jīng)知道是哪個文件了。 但是假設(shè)你不知道;該如何找出哪個文件或哪些文件占用了如此多的空間? 如果你執(zhí)行?git gc?命令,所有的對象將被放入一個包文件中,你可以通過運行?git verify-pack?命令,然后對輸出內(nèi)容的第三列(即文件大?。┻M行排序,從而找出這個大文件。 你也可以將這個命令的執(zhí)行結(jié)果通過管道傳送給?tail?命令,因為你只需要找到列在最后的幾個大對象。

$ 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

你可以看到這個大對象出現(xiàn)在返回結(jié)果的最底部:占用 5MB 空間。 為了找出具體是哪個文件,可以使用?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)在,你只需要從過去所有的樹中移除這個文件。 使用以下命令可以輕松地查看哪些提交對這個文件產(chǎn)生改動:

$ git log --oneline --branches -- git.tgz
dadf725 oops - removed large tarball
7b30847 add git tarball

現(xiàn)在,你必須重寫?7b30847?提交之后的所有提交來從 Git 歷史中完全移除這個文件。 為了執(zhí)行這個操作,我們要使用?filter-branch?命令,這個命令在?重寫歷史?中也使用過:

$ 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?選項類似于在?重寫歷史?中提到的的?--tree-filter?選項,不過這個選項并不會讓命令將修改在硬盤上檢出的文件,而只是修改在暫存區(qū)或索引中的文件。

你必須使用?git rm --cached?命令來移除文件,而不是通過類似?rm file?的命令 - 因為你需要從索引中移除它,而不是磁盤中。 還有一個原因是速度 - Git 在運行過濾器時,并不會檢出每個修訂版本到磁盤中,所以這個過程會非??臁?如果愿意的話,你也可以通過?--tree-filter?選項來完成同樣的任務。?git rm?命令的?--ignore-unmatch?選項告訴命令:如果嘗試刪除的模式不存在時,不提示錯誤。 最后,使用?filter-branch?選項來重寫自?7b30847?提交以來的歷史,也就是這個問題產(chǎn)生的地方。 否則,這個命令會從最舊的提交開始,這將會花費許多不必要的時間。

你的歷史中將不再包含對那個文件的引用。 不過,你的引用日志和你在?.git/refs/original?通過?filter-branch?選項添加的新引用中還存有對這個文件的引用,所以你必須移除它們?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 的值看出,這個大文件還在你的松散對象中,并沒有消失;但是它不會在推送或接下來的克隆中出現(xiàn),這才是最重要的。 如果真的想要刪除它,可以通過有?--expire?選項的?git prune?命令來完全地移除那個對象:

$ 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
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號