你時(shí)不時(shí)的需要進(jìn)行一些清理工作 ── 如減小一個(gè)倉(cāng)庫(kù)的大小,清理導(dǎo)入的庫(kù),或是恢復(fù)丟失的數(shù)據(jù)。本節(jié)將描述這類使用場(chǎng)景。
Git 會(huì)不定時(shí)地自動(dòng)運(yùn)行稱為 "auto gc" 的命令。大部分情況下該命令什么都不處理。不過(guò)要是存在太多松散對(duì)象 (loose object, 不在 packfile 中的對(duì)象) 或 packfile,Git 會(huì)進(jìn)行調(diào)用 git gc
命令。 gc
指垃圾收集 (garbage collect),此命令會(huì)做很多工作:收集所有松散對(duì)象并將它們存入 packfile,合并這些 packfile 進(jìn)一個(gè)大的 packfile,然后將不被任何 commit 引用并且已存在一段時(shí)間 (數(shù)月) 的對(duì)象刪除。
可以手工運(yùn)行 auto gc 命令:
$ git gc --auto
再次強(qiáng)調(diào),這個(gè)命令一般什么都不干。如果有 7,000 個(gè)左右的松散對(duì)象或是 50 個(gè)以上的 packfile,Git 才會(huì)真正調(diào)用 gc 命令。可能通過(guò)修改配置中的 gc.auto
和 gc.autopacklimit
來(lái)調(diào)整這兩個(gè)閾值。
gc
還會(huì)將所有引用 (references) 并入一個(gè)單獨(dú)文件。假設(shè)倉(cāng)庫(kù)中包含以下分支和標(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
這時(shí)如果運(yùn)行 git gc
, refs
下的所有文件都會(huì)消失。Git 會(huì)將這些文件挪到 .git/packed-refs
文件中去以提高效率,該文件是這個(gè)樣子的:
$ cat .git/packed-refs
# pack-refs with: peeled
cac0cab538b970a37ea1e769cbbde608743bc96d refs/heads/experiment
ab1afef80fac8e34258ff41fc1b867c702daa24b refs/heads/master
cac0cab538b970a37ea1e769cbbde608743bc96d refs/tags/v1.0
9585191f37f7b0fb9444f35a9bf50de191beadc2 refs/tags/v1.1
^1a410efbd13591db07496601ebc7a059dd55cfe9
當(dāng)更新一個(gè)引用時(shí),Git 不會(huì)修改這個(gè)文件,而是在 refs/heads
下寫(xiě)入一個(gè)新文件。當(dāng)查找一個(gè)引用的 SHA 時(shí),Git 首先在 refs
目錄下查找,如果未找到則到 packed-refs
文件中去查找。因此如果在 refs
目錄下找不到一個(gè)引用,該引用可能存到 packed-refs
文件中去了。
請(qǐng)留意文件最后以 ^
開(kāi)頭的那一行。這表示該行上一行的那個(gè)標(biāo)簽是一個(gè) annotated 標(biāo)簽,而該行正是那個(gè)標(biāo)簽所指向的 commit 。
在使用 Git 的過(guò)程中,有時(shí)會(huì)不小心丟失 commit 信息。這一般出現(xiàn)在以下情況下:強(qiáng)制刪除了一個(gè)分支而后又想重新使用這個(gè)分支,hard-reset 了一個(gè)分支從而丟棄了分支的部分 commit。如果這真的發(fā)生了,有什么辦法把丟失的 commit 找回來(lái)呢?
下面的示例演示了對(duì) test 倉(cāng)庫(kù)主分支進(jìn)行 hard-reset 到一個(gè)老版本的 commit 的操作,然后恢復(fù)丟失的 commit 。首先查看一下當(dāng)前的倉(cāng)庫(kù)狀態(tài):
$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
接著將 master
分支移回至中間的一個(gè) commit:
$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
HEAD is now at 1a410ef third commit
$ git log --pretty=oneline
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
這樣就丟棄了最新的兩個(gè) commit ── 包含這兩個(gè) commit 的分支不存在了?,F(xiàn)在要做的是找出最新的那個(gè) commit 的 SHA,然后添加一個(gè)指它它的分支。關(guān)鍵在于找出最新的 commit 的 SHA ── 你不大可能記住了這個(gè) SHA,是吧?
通常最快捷的辦法是使用 git reflog
工具。當(dāng)你 (在一個(gè)倉(cāng)庫(kù)下) 工作時(shí),Git 會(huì)在你每次修改了 HEAD 時(shí)悄悄地將改動(dòng)記錄下來(lái)。當(dāng)你提交或修改分支時(shí),reflog 就會(huì)更新。git update-ref
命令也可以更新 reflog,這是在本章前面的 "Git References" 部分我們使用該命令而不是手工將 SHA 值寫(xiě)入 ref 文件的理由。任何時(shí)間運(yùn)行 git reflog
命令可以查看當(dāng)前的狀態(tài):
$ git reflog
1a410ef HEAD@{0}: 1a410efbd13591db07496601ebc7a059dd55cfe9: updating HEAD
ab1afef HEAD@{1}: ab1afef80fac8e34258ff41fc1b867c702daa24b: updating HEAD
可以看到我們簽出的兩個(gè) commit ,但沒(méi)有更多的相關(guān)信息。運(yùn)行 git log -g
會(huì)輸出 reflog 的正常日志,從而顯示更多有用信息:
$ 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 a bit
看起來(lái)弄丟了的 commit 是底下那個(gè),這樣在那個(gè) commit 上創(chuàng)建一個(gè)新分支就能把它恢復(fù)過(guò)來(lái)。比方說(shuō),可以在那個(gè) commit (ab1afef) 上創(chuàng)建一個(gè)名為 recover-branch
的分支:
$ 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
酷!這樣有了一個(gè)跟原來(lái) master
一樣的 recover-branch
分支,最新的兩個(gè) commit 又找回來(lái)了。接著,假設(shè)引起 commit 丟失的原因并沒(méi)有記錄在 reflog 中 ── 可以通過(guò)刪除 recover-branch
和 reflog 來(lái)模擬這種情況。這樣最新的兩個(gè) commit 不會(huì)被任何東西引用到:
$ git branch -D recover-branch
$ rm -Rf .git/logs/
因?yàn)?reflog 數(shù)據(jù)是保存在 .git/logs/
目錄下的,這樣就沒(méi)有 reflog 了?,F(xiàn)在要怎樣恢復(fù) commit 呢?辦法之一是使用 git fsck
工具,該工具會(huì)檢查倉(cāng)庫(kù)的數(shù)據(jù)完整性。如果指定 --full
選項(xiàng),該命令顯示所有未被其他對(duì)象引用 (指向) 的所有對(duì)象:
$ git fsck --full
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293
本例中,可以從 dangling commit 找到丟失了的 commit。用相同的方法就可以恢復(fù)它,即創(chuàng)建一個(gè)指向該 SHA 的分支。
Git 有許多過(guò)人之處,不過(guò)有一個(gè)功能有時(shí)卻會(huì)帶來(lái)問(wèn)題:git clone
會(huì)將包含每一個(gè)文件的所有歷史版本的整個(gè)項(xiàng)目下載下來(lái)。如果項(xiàng)目包含的僅僅是源代碼的話這并沒(méi)有什么壞處,畢竟 Git 可以非常高效地壓縮此類數(shù)據(jù)。不過(guò)如果有人在某個(gè)時(shí)刻往項(xiàng)目中添加了一個(gè)非常大的文件,那們即便他在后來(lái)的提交中將此文件刪掉了,所有的簽出都會(huì)下載這個(gè)大文件。因?yàn)闅v史記錄中引用了這個(gè)文件,它會(huì)一直存在著。
當(dāng)你將 Subversion 或 Perforce 倉(cāng)庫(kù)轉(zhuǎn)換導(dǎo)入至 Git 時(shí)這會(huì)成為一個(gè)很嚴(yán)重的問(wèn)題。在此類系統(tǒng)中,(簽出時(shí)) 不會(huì)下載整個(gè)倉(cāng)庫(kù)歷史,所以這種情形不大會(huì)有不良后果。如果你從其他系統(tǒng)導(dǎo)入了一個(gè)倉(cāng)庫(kù),或是發(fā)覺(jué)一個(gè)倉(cāng)庫(kù)的尺寸遠(yuǎn)超出預(yù)計(jì),可以用下面的方法找到并移除大 (尺寸) 對(duì)象。
警告:此方法會(huì)破壞提交歷史。為了移除對(duì)一個(gè)大文件的引用,從最早包含該引用的 tree 對(duì)象開(kāi)始之后的所有 commit 對(duì)象都會(huì)被重寫(xiě)。如果在剛導(dǎo)入一個(gè)倉(cāng)庫(kù)并在其他人在此基礎(chǔ)上開(kāi)始工作之前這么做,那沒(méi)有什么問(wèn)題 ── 否則你不得不通知所有協(xié)作者 (貢獻(xiàn)者) 去衍合你新修改的 commit 。
為了演示這點(diǎn),往 test 倉(cāng)庫(kù)中加入一個(gè)大文件,然后在下次提交時(shí)將它刪除,接著找到并將這個(gè)文件從倉(cāng)庫(kù)中永久刪除。首先,加一個(gè)大文件進(jìn)去:
$ curl http://kernel.org/pub/software/scm/git/git-1.6.3.1.tar.bz2 > git.tbz2
$ git add git.tbz2
$ git commit -am 'added git tarball'
[master 6df7640] added git tarball
1 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 git.tbz2
喔,你并不想往項(xiàng)目中加進(jìn)一個(gè)這么大的 tar 包。最后還是去掉它:
$ git rm git.tbz2
rm 'git.tbz2'
$ git commit -m 'oops - removed large tarball'
[master da3f30d] oops - removed large tarball
1 files changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 git.tbz2
對(duì)倉(cāng)庫(kù)進(jìn)行 gc
操作,并查看占用了空間:
$ git gc
Counting objects: 21, done.
Delta compression using 2 threads.
Compressing objects: 100% (16/16), done.
Writing objects: 100% (21/21), done.
Total 21 (delta 3), reused 15 (delta 1)
可以運(yùn)行 count-objects
以查看使用了多少空間:
$ git count-objects -v
count: 4
size: 16
in-pack: 21
packs: 1
size-pack: 2016
prune-packable: 0
garbage: 0
size-pack
是以千字節(jié)為單位表示的 packfiles 的大小,因此已經(jīng)使用了 2MB 。而在這次提交之前僅用了 2K 左右 ── 顯然在這次提交時(shí)刪除文件并沒(méi)有真正將其從歷史記錄中刪除。每當(dāng)有人復(fù)制這個(gè)倉(cāng)庫(kù)去取得這個(gè)小項(xiàng)目時(shí),都不得不復(fù)制所有 2MB 數(shù)據(jù),而這僅僅因?yàn)槟阍?jīng)不小心加了個(gè)大文件。當(dāng)我們來(lái)解決這個(gè)問(wèn)題。
首先要找出這個(gè)文件。在本例中,你知道是哪個(gè)文件。假設(shè)你并不知道這一點(diǎn),要如何找出哪個(gè) (些) 文件占用了這么多的空間?如果運(yùn)行 git gc
,所有對(duì)象會(huì)存入一個(gè) packfile 文件;運(yùn)行另一個(gè)底層命令 git verify-pack
以識(shí)別出大對(duì)象,對(duì)輸出的第三列信息即文件大小進(jìn)行排序,還可以將輸出定向到 tail
命令,因?yàn)槟阒魂P(guān)心排在最后的那幾個(gè)最大的文件:
$ git verify-pack -v .git/objects/pack/pack-3f8c0...bb.idx | sort -k 3 -n | tail -3
e3f094f522629ae358806b17daf78246c27c007b blob 1486 734 4667
05408d195263d853f09dca71d55116663690c27c blob 12908 3478 1189
7a9eb2fba2b1811321254ac360970fc169ba2330 blob 2056716 2056872 5401
最底下那個(gè)就是那個(gè)大文件:2MB 。要查看這到底是哪個(gè)文件,可以使用第 7 章中已經(jīng)簡(jiǎn)單使用過(guò)的 rev-list
命令。若給 rev-list
命令傳入 --objects
選項(xiàng),它會(huì)列出所有 commit SHA 值,blob SHA 值及相應(yīng)的文件路徑??梢赃@樣查看 blob 的文件名:
$ git rev-list --objects --all | grep 7a9eb2fb
7a9eb2fba2b1811321254ac360970fc169ba2330 git.tbz2
接下來(lái)要將該文件從歷史記錄的所有 tree 中移除。很容易找出哪些 commit 修改了這個(gè)文件:
$ git log --pretty=oneline --branches -- git.tbz2
da3f30d019005479c99eb4c3406225613985a1db oops - removed large tarball
6df764092f3e7c8f5f94cbe08ee5cf42e92a0289 added git tarball
必須重寫(xiě)從 6df76
開(kāi)始的所有 commit 才能將文件從 Git 歷史中完全移除。這么做需要用到第 6 章中用過(guò)的 filter-branch
命令:
$ git filter-branch --index-filter \
'git rm --cached --ignore-unmatch git.tbz2' -- 6df7640^..
Rewrite 6df764092f3e7c8f5f94cbe08ee5cf42e92a0289 (1/2)rm 'git.tbz2'
Rewrite da3f30d019005479c99eb4c3406225613985a1db (2/2)
Ref 'refs/heads/master' was rewritten
--index-filter
選項(xiàng)類似于第 6 章中使用的 --tree-filter
選項(xiàng),但這里不是傳入一個(gè)命令去修改磁盤上簽出的文件,而是修改暫存區(qū)域或索引。不能用 rm file
命令來(lái)刪除一個(gè)特定文件,而是必須用 git rm --cached
來(lái)刪除它 ── 即從索引而不是磁盤刪除它。這樣做是出于速度考慮 ── 由于 Git 在運(yùn)行你的 filter 之前無(wú)需將所有版本簽出到磁盤上,這個(gè)操作會(huì)快得多。也可以用 --tree-filter
來(lái)完成相同的操作。git rm
的 --ignore-unmatch
選項(xiàng)指定當(dāng)你試圖刪除的內(nèi)容并不存在時(shí)不顯示錯(cuò)誤。最后,因?yàn)槟闱宄?wèn)題是從哪個(gè) commit 開(kāi)始的,使用 filter-branch
重寫(xiě)自 6df7640
這個(gè) commit 開(kāi)始的所有歷史記錄。不這么做的話會(huì)重寫(xiě)所有歷史記錄,花費(fèi)不必要的更多時(shí)間。
現(xiàn)在歷史記錄中已經(jīng)不包含對(duì)那個(gè)文件的引用了。不過(guò) reflog 以及運(yùn)行 filter-branch
時(shí) Git 往 .git/refs/original
添加的一些 refs 中仍有對(duì)它的引用,因此需要將這些引用刪除并對(duì)倉(cāng)庫(kù)進(jìn)行 repack 操作。在進(jìn)行 repack 前需要將所有對(duì)這些 commits 的引用去除:
$ rm -Rf .git/refs/original
$ rm -Rf .git/logs/
$ git gc
Counting objects: 19, done.
Delta compression using 2 threads.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (19/19), done.
Total 19 (delta 3), reused 16 (delta 1)
看一下節(jié)省了多少空間。
$ git count-objects -v
count: 8
size: 2040
in-pack: 19
packs: 1
size-pack: 7
prune-packable: 0
garbage: 0
repack 后倉(cāng)庫(kù)的大小減小到了 7K ,遠(yuǎn)小于之前的 2MB 。從 size 值可以看出大文件對(duì)象還在松散對(duì)象中,其實(shí)并沒(méi)有消失,不過(guò)這沒(méi)有關(guān)系,重要的是在再進(jìn)行推送或復(fù)制,這個(gè)對(duì)象不會(huì)再傳送出去。如果真的要完全把這個(gè)對(duì)象刪除,可以運(yùn)行 git prune --expire
命令。
更多建議: