Git详解之九 Git内部原理
以下內(nèi)容轉(zhuǎn)載自:http://www.open-open.com/lib/view/open1328070620202.html
?
?
Git 內(nèi)部原理
?
不管你是從前面的章節(jié)直接跳到了本章,還是讀完了其余各章一直到這,你都將在本章見識(shí) Git 的內(nèi)部工作原理和實(shí)現(xiàn)方式。我個(gè)人發(fā)現(xiàn)學(xué)習(xí)這些內(nèi)容對(duì)于理解 Git 的用處和強(qiáng)大是非常重要的,不過也有人認(rèn)為這些內(nèi)容對(duì)于初學(xué)者來說可能難以理解且過于復(fù)雜。正因如此我把這部分內(nèi)容放在最后一章,你在學(xué)習(xí)過程中可以先閱 讀這部分,也可以晚點(diǎn)閱讀這部分,這完全取決于你自己。
既然已經(jīng)讀到這了,就讓我們開始吧。首先要弄明白一點(diǎn),從根本上來講 Git 是一套內(nèi)容尋址 (content-addressable) 文件系統(tǒng),在此之上提供了一個(gè) VCS 用戶界面。馬上你就會(huì)學(xué)到這意味著什么。
早期的 Git (主要是 1.5 之前版本) 的用戶界面要比現(xiàn)在復(fù)雜得多,這是因?yàn)樗鼈?cè)重于成為文件系統(tǒng)而不是一套更精致的 VCS 。最近幾年改進(jìn)了 UI 從而使它跟其他任何系統(tǒng)一樣清晰易用。即便如此,還是經(jīng)常會(huì)有一些陳腔濫調(diào)提到早期 Git 的 UI 復(fù)雜又難學(xué)。
內(nèi)容尋址文件系統(tǒng)這一層相當(dāng)酷,在本章中我會(huì)先講解這部分。隨后你會(huì)學(xué)到傳輸機(jī)制和最終要使用的各種庫管理任務(wù)。
?
9.1? 底層命令 (Plumbing) 和高層命令 (Porcelain)
本書講解了使用?checkout,?branch,?remote?等共約 30 個(gè) Git 命令。然而由于 Git 一開始被設(shè)計(jì)成供 VCS 使用的工具集而不是一整套用戶友好的 VCS,它還包含了許多底層命令,這些命令用于以 UNIX 風(fēng)格使用或由腳本調(diào)用。這些命令一般被稱為 “plumbing” 命令(底層命令),其他的更友好的命令則被稱為 “porcelain” 命令(高層命令)。
本書前八章主要專門討論高層命令。本章將主要討論底層命令以理解 Git 的內(nèi)部工作機(jī)制、演示 Git 如何及為何要以這種方式工作。這些命令主要不是用來從命令行手工使用的,更多的是用來為其他工具和自定義腳本服務(wù)的。
當(dāng)你在一個(gè)新目錄或已有目錄內(nèi)執(zhí)行?git init?時(shí),Git 會(huì)創(chuàng)建一個(gè)?.git?目錄,幾乎所有 Git 存儲(chǔ)和操作的內(nèi)容都位于該目錄下。如果你要備份或復(fù)制一個(gè)庫,基本上將這一目錄拷貝至其他地方就可以了。本章基本上都討論該目錄下的內(nèi)容。該目錄結(jié)構(gòu)如下:
$ ls
HEAD
branches/
config
description
hooks/
index
info/
objects/
refs/ 該目錄下有可能還有其他文件,但這是一個(gè)全新的?git init?生成的庫,所以默認(rèn)情況下這些就是你能看到的結(jié)構(gòu)。新版本的 Git 不再使用branches?目錄,description?文件僅供 GitWeb 程序使用,所以不用關(guān)心這些內(nèi)容。config?文件包含了項(xiàng)目特有的配置選項(xiàng),info?目錄保存了一份不希望在 .gitignore 文件中管理的忽略模式 (ignored patterns) 的全局可執(zhí)行文件。hooks?目錄包住了第六章詳細(xì)介紹了的客戶端或服務(wù)端鉤子腳本。
另外還有四個(gè)重要的文件或目錄:HEAD?及?index?文件,objects?及refs?目錄。這些是 Git 的核心部分。objects?目錄存儲(chǔ)所有數(shù)據(jù)內(nèi)容,refs?目錄存儲(chǔ)指向數(shù)據(jù) (分支) 的提交對(duì)象的指針,HEAD?文件指向當(dāng)前分支,index?文件保存了暫存區(qū)域信息。馬上你將詳細(xì)了解 Git 是如何操縱這些內(nèi)容的。
?
9.2? Git 對(duì)象
Git 是一套內(nèi)容尋址文件系統(tǒng)。很不錯(cuò)。不過這是什么意思呢?這種說法的意思是,從內(nèi)部來看,Git 是簡單的 key-value 數(shù)據(jù)存儲(chǔ)。它允許插入任意類型的內(nèi)容,并會(huì)返回一個(gè)鍵值,通過該鍵值可以在任何時(shí)候再取出該內(nèi)容。可以通過底層命令hash-object?來示范這點(diǎn),傳一些數(shù)據(jù)給該命令,它會(huì)將數(shù)據(jù)保存在?.git?目錄并返回表示這些數(shù)據(jù)的鍵值。首先初使化一個(gè) Git 倉庫并確認(rèn)objects?目錄是空的:
$ mkdir test
$ cd test
$ git init
Initialized empty Git repository in /tmp/test/.git/
$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
$ find .git/objects -type f
$ Git 初始化了?objects?目錄,同時(shí)在該目錄下創(chuàng)建了?pack?和?info?子目錄,但是該目錄下沒有其他常規(guī)文件。我們往這個(gè) Git 數(shù)據(jù)庫里存儲(chǔ)一些文本:
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4 參數(shù)?-w?指示?hash-object?命令存儲(chǔ) (數(shù)據(jù)) 對(duì)象,若不指定這個(gè)參數(shù)該命令僅僅返回鍵值。--stdin?指定從標(biāo)準(zhǔn)輸入設(shè)備 (stdin) 來讀取內(nèi)容,若不指定這個(gè)參數(shù)則需指定一個(gè)要存儲(chǔ)的文件的路徑。該命令輸出長度為 40 個(gè)字符的校驗(yàn)和。這是個(gè) SHA-1 哈希值──其值為要存儲(chǔ)的數(shù)據(jù)加上你馬上會(huì)了解到的一種頭信息的校驗(yàn)和。現(xiàn)在可以查看到 Git 已經(jīng)存儲(chǔ)了數(shù)據(jù):
$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 可以在?objects?目錄下看到一個(gè)文件。這便是 Git 存儲(chǔ)數(shù)據(jù)內(nèi)容的方式──為每份內(nèi)容生成一個(gè)文件,取得該內(nèi)容與頭信息的 SHA-1 校驗(yàn)和,創(chuàng)建以該校驗(yàn)和前兩個(gè)字符為名稱的子目錄,并以 (校驗(yàn)和) 剩下 38 個(gè)字符為文件命名 (保存至子目錄下)。
通過?cat-file?命令可以將數(shù)據(jù)內(nèi)容取回。該命令是查看 Git 對(duì)象的瑞士軍刀。傳入?-p?參數(shù)可以讓該命令輸出數(shù)據(jù)內(nèi)容的類型:
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content 可以往 Git 中添加更多內(nèi)容并取回了。也可以直接添加文件。比方說可以對(duì)一個(gè)文件進(jìn)行簡單的版本控制。首先,創(chuàng)建一個(gè)新文件,并把文件內(nèi)容存儲(chǔ)到數(shù)據(jù)庫中:
$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30 接著往該文件中寫入一些新內(nèi)容并再次保存:
$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a 數(shù)據(jù)庫中已經(jīng)將文件的兩個(gè)新版本連同一開始的內(nèi)容保存下來了:
$ find .git/objects -type f
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 再將文件恢復(fù)到第一個(gè)版本:
$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
$ cat test.txt
version 1 或恢復(fù)到第二個(gè)版本:
$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt
$ cat test.txt
version 2 需要記住的是幾個(gè)版本的文件 SHA-1 值可能與實(shí)際的值不同,其次,存儲(chǔ)的并不是文件名而僅僅是文件內(nèi)容。這種對(duì)象類型稱為 blob 。通過傳遞 SHA-1 值給cat-file -t?命令可以讓 Git 返回任何對(duì)象的類型:
$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob tree (樹) 對(duì)象
接下去來看 tree 對(duì)象,tree 對(duì)象可以存儲(chǔ)文件名,同時(shí)也允許存儲(chǔ)一組文件。Git 以一種類似 UNIX 文件系統(tǒng)但更簡單的方式來存儲(chǔ)內(nèi)容。所有內(nèi)容以 tree 或 blob 對(duì)象存儲(chǔ),其中 tree 對(duì)象對(duì)應(yīng)于 UNIX 中的目錄,blob 對(duì)象則大致對(duì)應(yīng)于 inodes 或文件內(nèi)容。一個(gè)單獨(dú)的 tree 對(duì)象包含一條或多條 tree 記錄,每一條記錄含有一個(gè)指向 blob 或子 tree 對(duì)象的 SHA-1 指針,并附有該對(duì)象的權(quán)限模式 (mode)、類型和文件名信息。以 simplegit 項(xiàng)目為例,最新的 tree 可能是這個(gè)樣子:
$ git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README
100644 blob 8f94139338f9404f26296befa88755fc2598c289 Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 lib master^{tree}?表示?branch?分支上最新提交指向的 tree 對(duì)象。請(qǐng)注意?lib?子目錄并非一個(gè) blob 對(duì)象,而是一個(gè)指向別一個(gè) tree 對(duì)象的指針:
$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0
100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b simplegit.rb 從概念上來講,Git 保存的數(shù)據(jù)如圖 9-1 所示。
圖 9-1. Git 對(duì)象模型的簡化版
你可以自己創(chuàng)建 tree 。通常 Git 根據(jù)你的暫存區(qū)域或 index 來創(chuàng)建并寫入一個(gè) tree 。因此要?jiǎng)?chuàng)建一個(gè) tree 對(duì)象的話首先要通過將一些文件暫存從而創(chuàng)建一個(gè) index 。可以使用 plumbing 命令update-index?為一個(gè)單獨(dú)文件 ── test.txt 文件的第一個(gè)版本 ──????創(chuàng)建一個(gè) index????。通過該命令人為的將 test.txt 文件的首個(gè)版本加入到了一個(gè)新的暫存區(qū)域中。由于該文件原先并不在暫存區(qū)域中 (甚至就連暫存區(qū)域也還沒被創(chuàng)建出來呢) ,必須傳入--add?參數(shù);由于要添加的文件并不在當(dāng)前目錄下而是在數(shù)據(jù)庫中,必須傳入?--cacheinfo?參數(shù)。同時(shí)指定了文件模式,SHA-1 值和文件名:
$ git update-index --add --cacheinfo 100644 \83baae61804e65cc73a7201a7252750c76066a30 test.txt 在本例中,指定了文件模式為?100644,表明這是一個(gè)普通文件。其他可用的模式有:100755?表示可執(zhí)行文件,120000?表示符號(hào)鏈接。文件模式是從常規(guī)的 UNIX 文件模式中參考來的,但是沒有那么靈活 ── 上述三種模式僅對(duì) Git 中的文件 (blobs) 有效 (雖然也有其他模式用于目錄和子模塊)。
現(xiàn)在可以用?write-tree?命令將暫存區(qū)域的內(nèi)容寫到一個(gè) tree 對(duì)象了。無需?-w?參數(shù) ── 如果目標(biāo) tree 不存在,調(diào)用write-tree?會(huì)自動(dòng)根據(jù) index 狀態(tài)創(chuàng)建一個(gè) tree 對(duì)象。
$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt 可以這樣驗(yàn)證這確實(shí)是一個(gè) tree 對(duì)象:
$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree 再根據(jù) test.txt 的第二個(gè)版本以及一個(gè)新文件創(chuàng)建一個(gè)新 tree 對(duì)象:
$ echo 'new file' > new.txt
$ git update-index test.txt
$ git update-index --add new.txt 這時(shí)暫存區(qū)域中包含了 test.txt 的新版本及一個(gè)新文件 new.txt 。創(chuàng)建 (寫) 該 tree 對(duì)象 (將暫存區(qū)域或 index 狀態(tài)寫入到一個(gè) tree 對(duì)象),然后瞧瞧它的樣子:
$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt 請(qǐng)注意該 tree 對(duì)象包含了兩個(gè)文件記錄,且 test.txt 的 SHA 值是早先值的 “第二版” (1f7a7a)。來點(diǎn)更有趣的,你將把第一個(gè) tree 對(duì)象作為一個(gè)子目錄加進(jìn)該 tree 中。可以用read-tree?命令將 tree 對(duì)象讀到暫存區(qū)域中去。在這時(shí),通過傳一個(gè)?--prefix?參數(shù)給?read-tree,將一個(gè)已有的 tree 對(duì)象作為一個(gè)子 tree 讀到暫存區(qū)域中:
$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git write-tree
3c4e9cd789d88d8d89c1073707c3585e41b0e614
$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt 如果從剛寫入的新 tree 對(duì)象創(chuàng)建一個(gè)工作目錄,將得到位于工作目錄頂級(jí)的兩個(gè)文件和一個(gè)名為?bak?的子目錄,該子目錄包含了 test.txt 文件的第一個(gè)版本。可以將 Git 用來包含這些內(nèi)容的數(shù)據(jù)想象成如圖 9-2 所示的樣子。
圖 9-2. 當(dāng)前 Git 數(shù)據(jù)的內(nèi)容結(jié)構(gòu)
commit (提交) 對(duì)象
你現(xiàn)在有三個(gè) tree 對(duì)象,它們指向了你要跟蹤的項(xiàng)目的不同快照,可是先前的問題依然存在:必須記往三個(gè) SHA-1 值以獲得這些快照。你也沒有關(guān)于誰、何時(shí)以及為何保存了這些快照的信息。commit 對(duì)象為你保存了這些基本信息。
要?jiǎng)?chuàng)建一個(gè) commit 對(duì)象,使用?commit-tree?命令,指定一個(gè) tree 的 SHA-1,如果有任何前繼提交對(duì)象,也可以指定。從你寫的第一個(gè) tree 開始:
$ echo 'first commit' | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3d 通過?cat-file?查看這個(gè)新 commit 對(duì)象:
$ git cat-file -p fdf4fc3
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Scott Chacon 1243040974 -0700
committer Scott Chacon 1243040974 -0700first commit commit 對(duì)象有格式很簡單:指明了該時(shí)間點(diǎn)項(xiàng)目快照的頂層樹對(duì)象、作者/提交者信息(從 Git 設(shè)理發(fā)店的?user.name和user.email中獲得)以及當(dāng)前時(shí)間戳、一個(gè)空行,以及提交注釋信息。
接著再寫入另外兩個(gè) commit 對(duì)象,每一個(gè)都指定其之前的那個(gè) commit 對(duì)象:
$ echo 'second commit' | git commit-tree 0155eb -p fdf4fc3
cac0cab538b970a37ea1e769cbbde608743bc96d
$ echo 'third commit' | git commit-tree 3c4e9c -p cac0cab
1a410efbd13591db07496601ebc7a059dd55cfe9 每一個(gè) commit 對(duì)象都指向了你創(chuàng)建的樹對(duì)象快照。出乎意料的是,現(xiàn)在已經(jīng)有了真實(shí)的 Git 歷史了,所以如果運(yùn)行?git log?命令并指定最后那個(gè) commit 對(duì)象的 SHA-1 便可以查看歷史:
$ git log --stat 1a410e
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Author: Scott Chacon
Date: Fri May 22 18:15:24 2009 -0700third commitbak/test.txt | 1 +1 files changed, 1 insertions(+), 0 deletions(-)commit cac0cab538b970a37ea1e769cbbde608743bc96d
Author: Scott Chacon
Date: Fri May 22 18:14:29 2009 -0700second commitnew.txt | 1 +test.txt | 2 +-2 files changed, 2 insertions(+), 1 deletions(-)commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d
Author: Scott Chacon
Date: Fri May 22 18:09:34 2009 -0700first committest.txt | 1 +1 files changed, 1 insertions(+), 0 deletions(-) 真棒。你剛剛通過使用低級(jí)操作而不是那些普通命令創(chuàng)建了一個(gè) Git 歷史。這基本上就是運(yùn)行????git add?和?git commit命令時(shí) Git 進(jìn)行的工作????──保存修改了的文件的 blob,更新索引,創(chuàng)建 tree 對(duì)象,最后創(chuàng)建 commit 對(duì)象,這些 commit 對(duì)象指向了頂層 tree 對(duì)象以及先前的 commit 對(duì)象。這三類 Git 對(duì)象 ── blob,tree 以及 tree ── 都各自以文件的方式保存在.git/objects?目錄下。以下所列是目前為止樣例中的所有對(duì)象,每個(gè)對(duì)象后面的注釋里標(biāo)明了它們保存的內(nèi)容:
$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1 如果你按照以上描述進(jìn)行了操作,可以得到如圖 9-3 所示的對(duì)象圖。
圖 9-3. Git 目錄下的所有對(duì)象
對(duì)象存儲(chǔ)
之前我提到當(dāng)存儲(chǔ)數(shù)據(jù)內(nèi)容時(shí),同時(shí)會(huì)有一個(gè)文件頭被存儲(chǔ)起來。我們花些時(shí)間來看看 Git 是如何存儲(chǔ)對(duì)象的。你將看來如何通過 Ruby 腳本語言存儲(chǔ)一個(gè) blob 對(duì)象 (這里以字符串 “what is up, doc?” 為例) 。使用irb?命令進(jìn)入 Ruby 交互式模式:
$ irb
>> content = "what is up, doc?"
=> "what is up, doc?" Git 以對(duì)象類型為起始內(nèi)容構(gòu)造一個(gè)文件頭,本例中是一個(gè) blob。然后添加一個(gè)空格,接著是數(shù)據(jù)內(nèi)容的長度,最后是一個(gè)空字節(jié) (null byte):
>> header = "blob #{content.length}\0"
=> "blob 16\000" Git 將文件頭與原始數(shù)據(jù)內(nèi)容拼接起來,并計(jì)算拼接后的新內(nèi)容的 SHA-1 校驗(yàn)和。可以在 Ruby 中使用?require?語句導(dǎo)入 SHA1 digest 庫,然后調(diào)用Digest::SHA1.hexdigest()?方法計(jì)算字符串的 SHA-1 值:
>> store = header + content
=> "blob 16\000what is up, doc?"
>> require 'digest/sha1'
=> true
>> sha1 = Digest::SHA1.hexdigest(store)
=> "bd9dbf5aae1a3862dd1526723246b20206e5fc37" Git 用 zlib 對(duì)數(shù)據(jù)內(nèi)容進(jìn)行壓縮,在 Ruby 中可以用 zlib 庫來實(shí)現(xiàn)。首先需要導(dǎo)入該庫,然后用Zlib::Deflate.deflate()?對(duì)數(shù)據(jù)進(jìn)行壓縮:
>> require 'zlib'
=> true
>> zlib_content = Zlib::Deflate.deflate(store)
=> "x\234K\312\311OR04c(\317H,Q\310,V(-\320QH\311O\266\a\000_\034\a\235" 最后將用 zlib 壓縮后的內(nèi)容寫入磁盤。需要指定保存對(duì)象的路徑 (SHA-1 值的頭兩個(gè)字符作為子目錄名稱,剩余 38 個(gè)字符作為文件名保存至該子目錄中)。在 Ruby 中,如果子目錄不存在可以用FileUtils.mkdir_p()?函數(shù)創(chuàng)建它。接著用File.open?方法打開文件,并用?write()?方法將之前壓縮的內(nèi)容寫入該文件:
>> path = '.git/objects/' + sha1[0,2] + '/' + sha1[2,38]
=> ".git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37"
>> require 'fileutils'
=> true
>> FileUtils.mkdir_p(File.dirname(path))
=> ".git/objects/bd"
>> File.open(path, 'w') { |f| f.write zlib_content }
=> 32 這就行了 ── 你已經(jīng)創(chuàng)建了一個(gè)正確的 blob 對(duì)象。所有的 Git 對(duì)象都以這種方式存儲(chǔ),惟一的區(qū)別是類型不同 ── 除了字符串 blob,文件頭起始內(nèi)容還可以是 commit 或 tree 。不過雖然 blob 幾乎可以是任意內(nèi)容,commit 和 tree 的數(shù)據(jù)卻是有固定格式的。
?
9.3? Git References
你可以執(zhí)行像?git log 1a410e?這樣的命令來查看完整的歷史,但是這樣你就要記得?1a410e?是你最后一次提交,這樣才能在提交歷史中找到這些對(duì)象。你需要一個(gè)文件來用一個(gè)簡單的名字來記錄這些 SHA-1 值,這樣你就可以用這些指針而不是原來的 SHA-1 值去檢索了。
在 Git 中,我們稱之為“引用”(references 或者 refs,譯者注)。你可以在?.git/refs?目錄下面找到這些包含 SHA-1 值的文件。在這個(gè)項(xiàng)目里,這個(gè)目錄還沒不包含任何文件,但是包含這樣一個(gè)簡單的結(jié)構(gòu):
$ find .git/refs
.git/refs
.git/refs/heads
.git/refs/tags
$ find .git/refs -type f
$ 如果想要?jiǎng)?chuàng)建一個(gè)新的引用幫助你記住最后一次提交,技術(shù)上你可以這樣做:
$ echo "1a410efbd13591db07496601ebc7a059dd55cfe9" > .git/refs/heads/master 現(xiàn)在,你就可以在 Git 命令中使用你剛才創(chuàng)建的引用而不是 SHA-1 值:
$ git log --pretty=oneline master
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit 當(dāng)然,我們并不鼓勵(lì)你直接修改這些引用文件。如果你確實(shí)需要更新一個(gè)引用,Git 提供了一個(gè)安全的命令?update-ref:
$ git update-ref refs/heads/master 1a410efbd13591db07496601ebc7a059dd55cfe9 基本上 Git 中的一個(gè)分支其實(shí)就是一個(gè)指向某個(gè)工作版本一條 HEAD 記錄的指針或引用。你可以用這條命令創(chuàng)建一個(gè)指向第二次提交的分支:
$ git update-ref refs/heads/test cac0ca 這樣你的分支將會(huì)只包含那次提交以及之前的工作:
$ git log --pretty=oneline test
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit 現(xiàn)在,你的 Git 數(shù)據(jù)庫應(yīng)該看起來像圖 9-4 一樣。
圖 9-4. 包含分支引用的 Git 目錄對(duì)象
每當(dāng)你執(zhí)行?git branch (分支名稱)?這樣的命令,Git 基本上就是執(zhí)行?update-ref?命令,把你現(xiàn)在所在分支中最后一次提交的 SHA-1 值,添加到你要?jiǎng)?chuàng)建的分支的引用。
HEAD 標(biāo)記
現(xiàn)在的問題是,當(dāng)你執(zhí)行?git branch (分支名稱)?這條命令的時(shí)候,Git 怎么知道最后一次提交的 SHA-1 值呢?答案就是 HEAD 文件。HEAD 文件是一個(gè)指向你當(dāng)前所在分支的引用標(biāo)識(shí)符。這樣的引用標(biāo)識(shí)符——它看起來并不像一個(gè)普通的引用——其實(shí)并不包含 SHA-1 值,而是一個(gè)指向另外一個(gè)引用的指針。如果你看一下這個(gè)文件,通常你將會(huì)看到這樣的內(nèi)容:
$ cat .git/HEAD
ref: refs/heads/master 如果你執(zhí)行?git checkout test,Git 就會(huì)更新這個(gè)文件,看起來像這樣:
$ cat .git/HEAD
ref: refs/heads/test 當(dāng)你再執(zhí)行?git commit?命令,它就創(chuàng)建了一個(gè) commit 對(duì)象,把這個(gè) commit 對(duì)象的父級(jí)設(shè)置為 HEAD 指向的引用的 SHA-1 值。
你也可以手動(dòng)編輯這個(gè)文件,但是同樣有一個(gè)更安全的方法可以這樣做:symbolic-ref。你可以用下面這條命令讀取 HEAD 的值:
$ git symbolic-ref HEAD
refs/heads/master 你也可以設(shè)置 HEAD 的值:
$ git symbolic-ref HEAD refs/heads/test
$ cat .git/HEAD
ref: refs/heads/test 但是你不能設(shè)置成 refs 以外的形式:
$ git symbolic-ref HEAD test
fatal: Refusing to point HEAD outside of refs/ Tags
你剛剛已經(jīng)重溫過了 Git 的三個(gè)主要對(duì)象類型,現(xiàn)在這是第四種。Tag 對(duì)象非常像一個(gè) commit 對(duì)象——包含一個(gè)標(biāo)簽,一組數(shù)據(jù),一個(gè)消息和一個(gè)指針。最主要的區(qū)別就是 Tag 對(duì)象指向一個(gè) commit 而不是一個(gè) tree。它就像是一個(gè)分支引用,但是不會(huì)變化——永遠(yuǎn)指向同一個(gè) commit,僅僅是提供一個(gè)更加友好的名字。
正如我們?cè)诘诙滤懻摰?#xff0c;Tag 有兩種類型:annotated 和 lightweight 。你可以類似下面這樣的命令建立一個(gè) lightweight tag:
$ git update-ref refs/tags/v1.0 cac0cab538b970a37ea1e769cbbde608743bc96d 這就是 lightweight tag 的全部 —— 一個(gè)永遠(yuǎn)不會(huì)發(fā)生變化的分支。 annotated tag 要更復(fù)雜一點(diǎn)。如果你創(chuàng)建一個(gè) annotated tag,Git 會(huì)創(chuàng)建一個(gè) tag 對(duì)象,然后寫入一個(gè)指向指向它而不是直接指向 commit 的 reference。你可以這樣創(chuàng)建一個(gè) annotated tag(-a?參數(shù)表明這是一個(gè) annotated tag):
$ git tag -a v1.1 1a410efbd13591db07496601ebc7a059dd55cfe9 -m 'test tag' 這是所創(chuàng)建對(duì)象的 SHA-1 值:
$ cat .git/refs/tags/v1.1
9585191f37f7b0fb9444f35a9bf50de191beadc2 現(xiàn)在你可以運(yùn)行?cat-file?命令檢查這個(gè) SHA-1 值:
$ git cat-file -p 9585191f37f7b0fb9444f35a9bf50de191beadc2
object 1a410efbd13591db07496601ebc7a059dd55cfe9
type commit
tag v1.1
tagger Scott Chacon Sat May 23 16:48:58 2009 -0700test tag 值得注意的是這個(gè)對(duì)象指向你所標(biāo)記的 commit 對(duì)象的 SHA-1 值。同時(shí)需要注意的是它并不是必須要指向一個(gè) commit 對(duì)象;你可以標(biāo)記任何 Git 對(duì)象。例如,在 Git 的源代碼里,管理者添加了一個(gè) GPG 公鑰(這是一個(gè) blob 對(duì)象)對(duì)它做了一個(gè)標(biāo)簽。你就可以運(yùn)行:
$ git cat-file blob junio-gpg-pub 來查看 Git 源代碼倉庫中的公鑰. Linux kernel 也有一個(gè)不是指向 commit 對(duì)象的 tag —— 第一個(gè) tag 是在導(dǎo)入源代碼的時(shí)候創(chuàng)建的,它指向初始 tree (initial tree,譯者注)。
Remotes
你將會(huì)看到的第四種 reference 是 remote reference(遠(yuǎn)程引用,譯者注)。如果你添加了一個(gè) remote 然后推送代碼過去,Git 會(huì)把你最后一次推送到這個(gè) remote 的每個(gè)分支的值都記錄在refs/remotes?目錄下。例如,你可以添加一個(gè)叫做origin?的 remote 然后把你的?master?分支推送上去:
$ git remote add origin git@github.com:schacon/simplegit-progit.git
$ git push origin master
Counting objects: 11, done.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (7/7), 716 bytes, done.
Total 7 (delta 2), reused 4 (delta 1)
To git@github.com:schacon/simplegit-progit.gita11bef0..ca82a6d master -> master 然后查看?refs/remotes/origin/master?這個(gè)文件,你就會(huì)發(fā)現(xiàn)?origin?remote 中的master?分支就是你最后一次和服務(wù)器的通信。
$ cat .git/refs/remotes/origin/master
ca82a6dff817ec66f44342007202690a93763949 Remote 應(yīng)用和分支主要區(qū)別在于他們是不能被 check out 的。Git 把他們當(dāng)作是標(biāo)記這些了這些分支在服務(wù)器上最后狀態(tài)的一種書簽。
?
9.4? Packfiles
我們?cè)賮砜匆幌?test Git 倉庫。目前為止,有 11 個(gè)對(duì)象 ── 4 個(gè) blob,3 個(gè) tree,3 個(gè) commit 以及一個(gè) tag:
$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/95/85191f37f7b0fb9444f35a9bf50de191beadc2 # tag
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1 Git 用 zlib 壓縮文件內(nèi)容,因此這些文件并沒有占用太多空間,所有文件加起來總共僅用了 925 字節(jié)。接下去你會(huì)添加一些大文件以演示 Git 的一個(gè)很有意思的功能。將你之前用到過的 Grit 庫中的 repo.rb 文件加進(jìn)去 ── 這個(gè)源代碼文件大小約為 12K:
$ curl http://github.com/mojombo/grit/raw/master/lib/grit/repo.rb > repo.rb
$ git add repo.rb
$ git commit -m 'added repo.rb'
[master 484a592] added repo.rb3 files changed, 459 insertions(+), 2 deletions(-)delete mode 100644 bak/test.txtcreate mode 100644 repo.rbrewrite test.txt (100%) 如果查看一下生成的 tree,可以看到 repo.rb 文件的 blob 對(duì)象的 SHA-1 值:
$ git cat-file -p master^{tree}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 9bc1dc421dcd51b4ac296e3e5b6e2a99cf44391e repo.rb
100644 blob e3f094f522629ae358806b17daf78246c27c007b test.txt 然后可以用?git cat-file?命令查看這個(gè)對(duì)象有多大:
$ git cat-file -s 9bc1dc421dcd51b4ac296e3e5b6e2a99cf44391e
12898 稍微修改一下些文件,看會(huì)發(fā)生些什么:
$ echo '# testing' >> repo.rb
$ git commit -am 'modified repo a bit'
[master ab1afef] modified repo a bit1 files changed, 1 insertions(+), 0 deletions(-) 查看這個(gè) commit 生成的 tree,可以看到一些有趣的東西:
$ git cat-file -p master^{tree}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 05408d195263d853f09dca71d55116663690c27c repo.rb
100644 blob e3f094f522629ae358806b17daf78246c27c007b test.txt blob 對(duì)象與之前的已經(jīng)不同了。這說明雖然只是往一個(gè) 400 行的文件最后加入了一行內(nèi)容,Git 卻用一個(gè)全新的對(duì)象來保存新的文件內(nèi)容:
$ git cat-file -s 05408d195263d853f09dca71d55116663690c27c
12908 你的磁盤上有了兩個(gè)幾乎完全相同的 12K 的對(duì)象。如果 Git 只完整保存其中一個(gè),并保存另一個(gè)對(duì)象的差異內(nèi)容,豈不更好?
事實(shí)上 Git 可以那樣做。Git 往磁盤保存對(duì)象時(shí)默認(rèn)使用的格式叫松散對(duì)象 (loose object) 格式。Git 時(shí)不時(shí)地將這些對(duì)象打包至一個(gè)叫 packfile 的二進(jìn)制文件以節(jié)省空間并提高效率。當(dāng)倉庫中有太多的松散對(duì)象,或是手工調(diào)用git gc?命令,或推送至遠(yuǎn)程服務(wù)器時(shí),Git 都會(huì)這樣做。手工調(diào)用?git gc?命令讓 Git 將庫中對(duì)象打包并看會(huì)發(fā)生些什么:
$ git gc
Counting objects: 17, done.
Delta compression using 2 threads.
Compressing objects: 100% (13/13), done.
Writing objects: 100% (17/17), done.
Total 17 (delta 1), reused 10 (delta 0) 查看一下 objects 目錄,會(huì)發(fā)現(xiàn)大部分對(duì)象都不在了,與此同時(shí)出現(xiàn)了兩個(gè)新文件:
$ find .git/objects -type f
.git/objects/71/08f7ecb345ee9d0084193f147cdad4d2998293
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/info/packs
.git/objects/pack/pack-7a16e4488ae40c7d2bc56ea2bd43e25212a66c45.idx
.git/objects/pack/pack-7a16e4488ae40c7d2bc56ea2bd43e25212a66c45.pack 仍保留著的幾個(gè)對(duì)象是未被任何 commit 引用的 blob ── 在此例中是你之前創(chuàng)建的 “what is up, doc?” 和 “test content” 這兩個(gè)示例 blob。你從沒將他們添加至任何 commit,所以 Git 認(rèn)為它們是 “懸空” 的,不會(huì)將它們打包進(jìn) packfile 。
剩下的文件是新創(chuàng)建的 packfile 以及一個(gè)索引。packfile 文件包含了剛才從文件系統(tǒng)中移除的所有對(duì)象。索引文件包含了 packfile 的偏移信息,這樣就可以快速定位任意一個(gè)指定對(duì)象。有意思的是運(yùn)行gc?命令前磁盤上的對(duì)象大小約為 12K ,而這個(gè)新生成的 packfile 僅為 6K 大小。通過打包對(duì)象減少了一半磁盤使用空間。
Git 是如何做到這點(diǎn)的?Git 打包對(duì)象時(shí),會(huì)查找命名及尺寸相近的文件,并只保存文件不同版本之間的差異內(nèi)容。可以查看一下 packfile ,觀察它是如何節(jié)省空間的。git verify-pack?命令用于顯示已打包的內(nèi)容:
$ git verify-pack -v \.git/objects/pack/pack-7a16e4488ae40c7d2bc56ea2bd43e25212a66c45.idx
0155eb4229851634a0f03eb265b69f5a2d56f341 tree 71 76 5400
05408d195263d853f09dca71d55116663690c27c blob 12908 3478 874
09f01cea547666f58d6a8d809583841a7c6f0130 tree 106 107 5086
1a410efbd13591db07496601ebc7a059dd55cfe9 commit 225 151 322
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a blob 10 19 5381
3c4e9cd789d88d8d89c1073707c3585e41b0e614 tree 101 105 5211
484a59275031909e19aadb7c92262719cfcdf19a commit 226 153 169
83baae61804e65cc73a7201a7252750c76066a30 blob 10 19 5362
9585191f37f7b0fb9444f35a9bf50de191beadc2 tag 136 127 5476
9bc1dc421dcd51b4ac296e3e5b6e2a99cf44391e blob 7 18 5193 1
05408d195263d853f09dca71d55116663690c27c \ab1afef80fac8e34258ff41fc1b867c702daa24b commit 232 157 12
cac0cab538b970a37ea1e769cbbde608743bc96d commit 226 154 473
d8329fc1cc938780ffdd9f94e0d364e0ea74f579 tree 36 46 5316
e3f094f522629ae358806b17daf78246c27c007b blob 1486 734 4352
f8f51d7d8a1760462eca26eebafde32087499533 tree 106 107 749
fa49b077972391ad58037050f2a75f74e3671e92 blob 9 18 856
fdf4fc3344e67ab068f836878b6c4951e3b15f3d commit 177 122 627
chain length = 1: 1 object
pack-7a16e4488ae40c7d2bc56ea2bd43e25212a66c45.pack: ok 如果你還記得的話,?9bc1d?這個(gè) blob 是 repo.rb 文件的第一個(gè)版本,這個(gè) blob 引用了?05408?這個(gè) blob,即該文件的第二個(gè)版本。命令輸出內(nèi)容的第三列顯示的是對(duì)象大小,可以看到05408?占用了 12K 空間,而?9bc1d?僅為 7 字節(jié)。非常有趣的是第二個(gè)版本才是完整保存文件內(nèi)容的對(duì)象,而第一個(gè)版本是以差異方式保存的 ── 這是因?yàn)榇蟛糠智闆r下需要快速訪問文件的最新版本。
最妙的是可以隨時(shí)進(jìn)行重新打包。Git 自動(dòng)定期對(duì)倉庫進(jìn)行重新打包以節(jié)省空間。當(dāng)然也可以手工運(yùn)行?git gc?命令來這么做。
?
9.5? The Refspec
這本書讀到這里,你已經(jīng)使用過一些簡單的遠(yuǎn)程分支到本地引用的映射方式了,這種映射可以更為復(fù)雜。 假設(shè)你像這樣添加了一項(xiàng)遠(yuǎn)程倉庫:
$ git remote add origin git@github.com:schacon/simplegit-progit.git 它在你的?.git/config?文件中添加了一節(jié),指定了遠(yuǎn)程的名稱 (origin), 遠(yuǎn)程倉庫的URL地址,和用于獲取操作的 Refspec:
[remote "origin"]url = git@github.com:schacon/simplegit-progit.gitfetch = +refs/heads/*:refs/remotes/origin/* Refspec 的格式是一個(gè)可選的?+?號(hào),接著是?:?的格式,這里?是遠(yuǎn)端上的引用格式,?是將要記錄在本地的引用格式。可選的?+?號(hào)告訴 Git 在即使不能快速演進(jìn)的情況下,也去強(qiáng)制更新它。
缺省情況下 refspec 會(huì)被?git remote add?命令所自動(dòng)生成, Git 會(huì)獲取遠(yuǎn)端上?refs/heads/?下面的所有引用,并將它寫入到本地的refs/remotes/origin/. 所以,如果遠(yuǎn)端上有一個(gè)?master?分支,你在本地可以通過下面這種方式來訪問它的歷史記錄:
$ git log origin/master
$ git log remotes/origin/master
$ git log refs/remotes/origin/master 它們?nèi)堑葍r(jià)的,因?yàn)?Git 把它們都擴(kuò)展成?refs/remotes/origin/master.
如果你想讓 Git 每次只拉取遠(yuǎn)程的?master?分支,而不是遠(yuǎn)程的所有分支,你可以把 fetch 這一行修改成這樣:
fetch = +refs/heads/master:refs/remotes/origin/master 這是?git fetch?操作對(duì)這個(gè)遠(yuǎn)端的缺省 refspec 值。而如果你只想做一次該操作,也可以在命令行上指定這個(gè) refspec. 如可以這樣拉取遠(yuǎn)程的master?分支到本地的?origin/mymaster?分支:
$ git fetch origin master:refs/remotes/origin/mymaster 你也可以在命令行上指定多個(gè) refspec. 像這樣可以一次獲取遠(yuǎn)程的多個(gè)分支:
$ git fetch origin master:refs/remotes/origin/mymaster \topic:refs/remotes/origin/topic
From git@github.com:schacon/simplegit! [rejected] master -> origin/mymaster (non fast forward)* [new branch] topic -> origin/topic 在這個(gè)例子中,?master?分支因?yàn)椴皇且粋€(gè)可以快速演進(jìn)的引用而拉取操作被拒絕。你可以在 refspec 之前使用一個(gè)?+?號(hào)來重載這種行為。
你也可以在配置文件中指定多個(gè) refspec. 如你想在每次獲取時(shí)都獲取?master?和?experiment?分支,就添加兩行:
[remote "origin"]url = git@github.com:schacon/simplegit-progit.gitfetch = +refs/heads/master:refs/remotes/origin/masterfetch = +refs/heads/experiment:refs/remotes/origin/experiment 但是這里不能使用部分通配符,像這樣就是不合法的:
fetch = +refs/heads/qa*:refs/remotes/origin/qa* 但無論如何,你可以使用命名空間來達(dá)到這個(gè)目的。如你有一個(gè)QA組,他們推送一系列分支,你想每次獲取?master?分支和QA組的所有分支,你可以使用這樣的配置段落:
[remote "origin"]url = git@github.com:schacon/simplegit-progit.gitfetch = +refs/heads/master:refs/remotes/origin/masterfetch = +refs/heads/qa/*:refs/remotes/origin/qa/* 如果你的工作流很復(fù)雜,有QA組推送的分支、開發(fā)人員推送的分支、和集成人員推送的分支,并且他們?cè)谶h(yuǎn)程分支上協(xié)作,你可以采用這種方式為他們創(chuàng)建各自的命名空間。
推送 Refspec
采用命名空間的方式確實(shí)很棒,但QA組成員第1次是如何將他們的分支推送到?qa/?空間里面的呢?答案是你可以使用 refspec 來推送。
如果QA組成員想把他們的?master?分支推送到遠(yuǎn)程的?qa/master?分支上,可以這樣運(yùn)行:
$ git push origin master:refs/heads/qa/master 如果他們想讓 Git 每次運(yùn)行?git push origin?時(shí)都這樣自動(dòng)推送,他們可以在配置文件中添加?push?值:
[remote "origin"]url = git@github.com:schacon/simplegit-progit.gitfetch = +refs/heads/*:refs/remotes/origin/*push = refs/heads/master:refs/heads/qa/master 這樣,就會(huì)讓?git push origin?缺省就把本地的?master?分支推送到遠(yuǎn)程的?qa/master?分支上。
刪除引用
你也可以使用 refspec 來刪除遠(yuǎn)程的引用,是通過運(yùn)行這樣的命令:
$ git push origin :topic 因?yàn)?refspec 的格式是?:, 通過把?部分留空的方式,這個(gè)意思是是把遠(yuǎn)程的topic?分支變成空,也就是刪除它。
?
9.6? 傳輸協(xié)議
Git 可以以兩種主要的方式跨越兩個(gè)倉庫傳輸數(shù)據(jù):基于HTTP協(xié)議之上,和?file://,?ssh://, 和git://?等智能傳輸協(xié)議。這一節(jié)帶你快速瀏覽這兩種主要的協(xié)議操作過程。
啞協(xié)議
Git 基于HTTP之上傳輸通常被稱為啞協(xié)議,這是因?yàn)樗诜?wù)端不需要有針對(duì) Git 特有的代碼。這個(gè)獲取過程僅僅是一系列GET請(qǐng)求,客戶端可以假定服務(wù)端的Git倉庫中的布局。讓我們以 simplegit 庫來看看http-fetch?的過程:
$ git clone http://github.com/schacon/simplegit-progit.git 它做的第1件事情就是獲取?info/refs?文件。這個(gè)文件是在服務(wù)端運(yùn)行了?update-server-info?所生成的,這也解釋了為什么在服務(wù)端要想使用HTTP傳輸,必須要開啟post-receive?鉤子:
=> GET info/refs
ca82a6dff817ec66f44342007202690a93763949 refs/heads/master 現(xiàn)在你有一個(gè)遠(yuǎn)端引用和SHA值的列表。下一步是尋找HEAD引用,這樣你就知道了在完成后,什么應(yīng)該被檢出到工作目錄:
=> GET HEAD
ref: refs/heads/master 這說明在完成獲取后,需要檢出?master?分支。 這時(shí),已經(jīng)可以開始漫游操作了。因?yàn)槟愕钠瘘c(diǎn)是在?info/refs?文件中所提到的ca82a6?commit 對(duì)象,你的開始操作就是獲取它:
=> GET objects/ca/82a6dff817ec66f44342007202690a93763949
(179 bytes of binary data) 然后你取回了這個(gè)對(duì)象 - 這在服務(wù)端是一個(gè)松散格式的對(duì)象,你使用的是靜態(tài)的 HTTP GET 請(qǐng)求獲取的。可以使用 zlib 解壓縮它,去除其頭部,查看它的 commmit 內(nèi)容:
$ git cat-file -p ca82a6dff817ec66f44342007202690a93763949
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
author Scott Chacon 1205815931 -0700
committer Scott Chacon 1240030591 -0700changed the version number 這樣,就得到了兩個(gè)需要進(jìn)一步獲取的對(duì)象 -?cfda3b?是這個(gè) commit 對(duì)象所對(duì)應(yīng)的 tree 對(duì)象,和?085bb3?是它的父對(duì)象;
=> GET objects/08/5bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
(179 bytes of data) 這樣就取得了這它的下一步 commit 對(duì)象,再抓取 tree 對(duì)象:
=> GET objects/cf/da3bf379e4f8dba8717dee55aab78aef7f4daf
(404 - Not Found) Oops - 看起來這個(gè) tree 對(duì)象在服務(wù)端并不以松散格式對(duì)象存在,所以得到了404響應(yīng),代表在HTTP服務(wù)端沒有找到該對(duì)象。這有好幾個(gè)原因 - 這個(gè)對(duì)象可能在替代倉庫里面,或者在打包文件里面, Git 會(huì)首先檢查任何列出的替代倉庫:
=> GET objects/info/http-alternates
(empty file) 如果這返回了幾個(gè)替代倉庫列表,那么它會(huì)去那些地方檢查松散格式對(duì)象和文件 - 這是一種在軟件分叉之間共享對(duì)象以節(jié)省磁盤的好方法。然而,在這個(gè)例子中,沒有替代倉庫。所以你所需要的對(duì)象肯定在某個(gè)打包文件中。要檢查服務(wù)端有哪些打包格式文件,你需要獲取objects/info/packs?文件,這里面包含有打包文件列表(是的,它也是被?update-server-info?所生成的);
=> GET objects/info/packs
P pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack 這里服務(wù)端只有一個(gè)打包文件,所以你要的對(duì)象顯然就在里面。但是你可以先檢查它的索引文件以確認(rèn)。這在服務(wù)端有多個(gè)打包文件時(shí)也很有用,因?yàn)檫@樣就可以先檢查你所需要的對(duì)象空間是在哪一個(gè)打包文件里面了:
=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.idx
(4k of binary data) 現(xiàn)在你有了這個(gè)打包文件的索引,你可以看看你要的對(duì)象是否在里面 - 因?yàn)樗饕募谐隽诉@個(gè)打包文件所包含的所有對(duì)象的SHA值,和該對(duì)象存在于打包文件中的偏移量,所以你只需要簡單地獲取整個(gè)打包文件:
=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack
(13k of binary data) 現(xiàn)在你也有了這個(gè) tree 對(duì)象,你可以繼續(xù)在 commit 對(duì)象上漫游。它們?nèi)慷荚谶@個(gè)你已經(jīng)下載到的打包文件里面,所以你不用繼續(xù)向服務(wù)端請(qǐng)求更多下載了。 在這完成之后,由于下載開始時(shí)已探明HEAD引用是指向master?分支, Git 會(huì)將它檢出到工作目錄。
整個(gè)過程看起來就像這樣:
$ git clone http://github.com/schacon/simplegit-progit.git
Initialized empty Git repository in /private/tmp/simplegit-progit/.git/
got ca82a6dff817ec66f44342007202690a93763949
walk ca82a6dff817ec66f44342007202690a93763949
got 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Getting alternates list for http://github.com/schacon/simplegit-progit.git
Getting pack list for http://github.com/schacon/simplegit-progit.git
Getting index for pack 816a9b2334da9953e530f27bcac22082a9f5b835
Getting pack 816a9b2334da9953e530f27bcac22082a9f5b835which contains cfda3bf379e4f8dba8717dee55aab78aef7f4daf
walk 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
walk a11bef06a3f659402fe7563abf99ad00de2209e6 智能協(xié)議
這個(gè)HTTP方法是很簡單但效率不是很高。使用智能協(xié)議是傳送數(shù)據(jù)的更常用的方法。這些協(xié)議在遠(yuǎn)端都有Git智能型進(jìn)程在服務(wù) - 它可以讀出本地?cái)?shù)據(jù)并計(jì)算出客戶端所需要的,并生成合適的數(shù)據(jù)給它,這有兩類傳輸數(shù)據(jù)的進(jìn)程:一對(duì)用于上傳數(shù)據(jù)和一對(duì)用于下載。
上傳數(shù)據(jù)
為了上傳數(shù)據(jù)至遠(yuǎn)端, Git 使用?send-pack?和?receive-pack?進(jìn)程。這個(gè)?send-pack?進(jìn)程運(yùn)行在客戶端上,它連接至遠(yuǎn)端運(yùn)行的?receive-pack?進(jìn)程。
舉例來說,你在你的項(xiàng)目上運(yùn)行了?git push origin master, 并且?origin?被定義為一個(gè)使用SSH協(xié)議的URL。 Git 會(huì)使用send-pack?進(jìn)程,它會(huì)啟動(dòng)一個(gè)基于SSH的連接到服務(wù)器。它嘗試像這樣透過SSH在服務(wù)端運(yùn)行命令:
$ ssh -x git@github.com "git-receive-pack 'schacon/simplegit-progit.git'"
005bca82a6dff817ec66f4437202690a93763949 refs/heads/master report-status delete-refs
003e085bb3bcb608e1e84b2432f8ecbe6306e7e7 refs/heads/topic
0000 這里的?git-receive-pack?命令會(huì)立即對(duì)它所擁有的每一個(gè)引用響應(yīng)一行 - 在這個(gè)例子中,只有?master?分支和它的SHA值。這里第1行也包含了服務(wù)端的能力列表(這里是report-status?和?delete-refs)。
每一行以4字節(jié)的十六進(jìn)制開始,用于指定整行的長度。你看到第1行以005b開始,這在十六進(jìn)制中表示91,意味著第1行有91字節(jié)長。下一行以003e起始,表示有62字節(jié)長,所以需要讀剩下的62字節(jié)。再下一行是0000開始,表示服務(wù)器已完成了引用列表過程。
現(xiàn)在它知道了服務(wù)端的狀態(tài),你的?send-pack?進(jìn)程會(huì)判斷哪些 commit 是它所擁有但服務(wù)端沒有的。針對(duì)每個(gè)引用,這次推送都會(huì)告訴對(duì)端的receive-pack?這個(gè)信息。舉例說,如果你在更新?master?分支,并且增加?experiment?分支,這個(gè)send-pack?將會(huì)是像這樣:
0085ca82a6dff817ec66f44342007202690a93763949 15027957951b64cf874c3557a0f3547bd83b3ff6 refs/heads/master report-status
00670000000000000000000000000000000000000000 cdfdb42577e2506715f8cfeacdbabc092bf63e8d refs/heads/experiment
0000 這里的全’0’的SHA-1值表示之前沒有過這個(gè)對(duì)象 - 因?yàn)槟闶窃谔砑有碌?experiment 引用。如果你在刪除一個(gè)引用,你會(huì)看到相反的: 就是右邊是全’0’。
Git 針對(duì)每個(gè)引用發(fā)送這樣一行信息,就是舊的SHA值,新的SHA值,和將要更新的引用的名稱。第1行還會(huì)包含有客戶端的能力。下一步,客戶端會(huì)發(fā)送一個(gè)所有那些服務(wù)端所沒有的對(duì)象的一個(gè)打包文件。最后,服務(wù)端以成功(或者失敗)來響應(yīng):
000Aunpack ok 下載數(shù)據(jù)
當(dāng)你在下載數(shù)據(jù)時(shí),?fetch-pack?和?upload-pack?進(jìn)程就起作用了。客戶端啟動(dòng)?fetch-pack?進(jìn)程,連接至遠(yuǎn)端的?upload-pack?進(jìn)程,以協(xié)商后續(xù)數(shù)據(jù)傳輸過程。
在遠(yuǎn)端倉庫有不同的方式啟動(dòng)?upload-pack?進(jìn)程。你可以使用與?receive-pack?相同的透過SSH管道的方式,也可以通過 Git 后臺(tái)來啟動(dòng)這個(gè)進(jìn)程,它默認(rèn)監(jiān)聽在9418號(hào)端口上。這里fetch-pack?進(jìn)程在連接后像這樣向后臺(tái)發(fā)送數(shù)據(jù):
003fgit-upload-pack schacon/simplegit-progit.git\0host=myserver.com\0 它也是以4字節(jié)指定后續(xù)字節(jié)長度的方式開始,然后是要運(yùn)行的命令,和一個(gè)空字節(jié),然后是服務(wù)端的主機(jī)名,再跟隨一個(gè)最后的空字節(jié)。 Git 后臺(tái)進(jìn)程會(huì)檢查這個(gè)命令是否可以運(yùn)行,以及那個(gè)倉庫是否存在,以及是否具有公開權(quán)限。如果所有檢查都通過了,它會(huì)啟動(dòng)這個(gè)upload-pack?進(jìn)程并將客戶端的請(qǐng)求移交給它。
如果你透過SSH使用獲取功能,?fetch-pack?會(huì)像這樣運(yùn)行:
$ ssh -x git@github.com "git-upload-pack 'schacon/simplegit-progit.git'" 不管哪種方式,在?fetch-pack?連接之后,?upload-pack?都會(huì)以這種形式返回:
0088ca82a6dff817ec66f44342007202690a93763949 HEAD\0multi_ack thin-pack \side-band side-band-64k ofs-delta shallow no-progress include-tag
003fca82a6dff817ec66f44342007202690a93763949 refs/heads/master
003e085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 refs/heads/topic
0000 這與?receive-pack?響應(yīng)很類似,但是這里指的能力是不同的。而且它還會(huì)指出HEAD引用,讓客戶端可以檢查是否是一份克隆。
在這里,?fetch-pack?進(jìn)程檢查它自己所擁有的對(duì)象和所有它需要的對(duì)象,通過發(fā)送 “want” 和所需對(duì)象的SHA值,發(fā)送 “have” 和所有它已擁有的對(duì)象的SHA值。在列表完成時(shí),再發(fā)送 “done” 通知upload-pack?進(jìn)程開始發(fā)送所需對(duì)象的打包文件。這個(gè)過程看起來像這樣:
0054want ca82a6dff817ec66f44342007202690a93763949 ofs-delta
0032have 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
0000
0009done 這是傳輸協(xié)議的一個(gè)很基礎(chǔ)的例子,在更復(fù)雜的例子中,客戶端可能會(huì)支持?multi_ack?或者?side-band?能力;但是這個(gè)例子中展示了智能協(xié)議的基本交互過程。
?
9.7? 維護(hù)及數(shù)據(jù)恢復(fù)
你時(shí)不時(shí)的需要進(jìn)行一些清理工作 ── 如減小一個(gè)倉庫的大小,清理導(dǎo)入的庫,或是恢復(fù)丟失的數(shù)據(jù)。本節(jié)將描述這類使用場景。
維護(hù)
Git 會(huì)不定時(shí)地自動(dòng)運(yùn)行稱為 “auto gc” 的命令。大部分情況下該命令什么都不處理。不過要是存在太多松散對(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 命令。可能通過修改配置中的gc.auto?和?gc.autopacklimit?來調(diào)整這兩個(gè)閾值。
gc?還會(huì)將所有引用 (references) 并入一個(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 這時(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?下寫入一個(gè)新文件。當(dāng)查找一個(gè)引用的 SHA 時(shí),Git 首先在refs?目錄下查找,如果未找到則到?packed-refs?文件中去查找。因此如果在?refs?目錄下找不到一個(gè)引用,該引用可能存到packed-refs?文件中去了。
請(qǐng)留意文件最后以?^?開頭的那一行。這表示該行上一行的那個(gè)標(biāo)簽是一個(gè) annotated 標(biāo)簽,而該行正是那個(gè)標(biāo)簽所指向的 commit 。
數(shù)據(jù)恢復(fù)
在使用 Git 的過程中,有時(shí)會(huì)不小心丟失 commit 信息。這一般出現(xiàn)在以下情況下:強(qiáng)制刪除了一個(gè)分支而后又想重新使用這個(gè)分支,hard-reset 了一個(gè)分支從而丟棄了分支的部分 commit。如果這真的發(fā)生了,有什么辦法把丟失的 commit 找回來呢?
下面的示例演示了對(duì) test 倉庫主分支進(jìn)行 hard-reset 到一個(gè)老版本的 commit 的操作,然后恢復(fù)丟失的 commit 。首先查看一下當(dāng)前的倉庫狀態(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 的分支不存在了。現(xiàn)在要做的是找出最新的那個(gè) commit 的 SHA,然后添加一個(gè)指它它的分支。關(guān)鍵在于找出最新的 commit 的 SHA ── 你不大可能記住了這個(gè) SHA,是吧?
通常最快捷的辦法是使用?git reflog?工具。當(dāng)你 (在一個(gè)倉庫下) 工作時(shí),Git 會(huì)在你每次修改了 HEAD 時(shí)悄悄地將改動(dòng)記錄下來。當(dāng)你提交或修改分支時(shí),reflog 就會(huì)更新。git update-ref?命令也可以更新 reflog,這是在本章前面的 “Git References” 部分我們使用該命令而不是手工將 SHA 值寫入 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 ,但沒有更多的相關(guān)信息。運(yùn)行?git log -g?會(huì)輸出 reflog 的正常日志,從而顯示更多有用信息:
$ git log -g
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Reflog: HEAD@{0} (Scott Chacon )
Reflog message: updating HEAD
Author: Scott Chacon
Date: Fri May 22 18:22:37 2009 -0700third commitcommit ab1afef80fac8e34258ff41fc1b867c702daa24b
Reflog: HEAD@{1} (Scott Chacon )
Reflog message: updating HEAD
Author: Scott Chacon
Date: Fri May 22 18:15:24 2009 -0700modified repo a bit 看起來弄丟了的 commit 是底下那個(gè),這樣在那個(gè) commit 上創(chuàng)建一個(gè)新分支就能把它恢復(fù)過來。比方說,可以在那個(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è)跟原來?master?一樣的?recover-branch?分支,最新的兩個(gè) commit 又找回來了。接著,假設(shè)引起 commit 丟失的原因并沒有記錄在 reflog 中 ── 可以通過刪除recover-branch?和 reflog 來模擬這種情況。這樣最新的兩個(gè) commit 不會(huì)被任何東西引用到:
$ git branch -D recover-branch
$ rm -Rf .git/logs/ 因?yàn)?reflog 數(shù)據(jù)是保存在?.git/logs/?目錄下的,這樣就沒有 reflog 了。現(xiàn)在要怎樣恢復(fù) commit 呢?辦法之一是使用git fsck?工具,該工具會(huì)檢查倉庫的數(shù)據(jù)完整性。如果指定?--ful?選項(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 的分支。
移除對(duì)象
Git 有許多過人之處,不過有一個(gè)功能有時(shí)卻會(huì)帶來問題:git clone?會(huì)將包含每一個(gè)文件的所有歷史版本的整個(gè)項(xiàng)目下載下來。如果項(xiàng)目包含的僅僅是源代碼的話這并沒有什么壞處,畢竟 Git 可以非常高效地壓縮此類數(shù)據(jù)。不過如果有人在某個(gè)時(shí)刻往項(xiàng)目中添加了一個(gè)非常大的文件,那們即便他在后來的提交中將此文件刪掉了,所有的簽出都會(huì)下載這個(gè) 大文件。因?yàn)闅v史記錄中引用了這個(gè)文件,它會(huì)一直存在著。
當(dāng)你將 Subversion 或 Perforce 倉庫轉(zhuǎn)換導(dǎo)入至 Git 時(shí)這會(huì)成為一個(gè)很嚴(yán)重的問題。在此類系統(tǒng)中,(簽出時(shí)) 不會(huì)下載整個(gè)倉庫歷史,所以這種情形不大會(huì)有不良后果。如果你從其他系統(tǒng)導(dǎo)入了一個(gè)倉庫,或是發(fā)覺一個(gè)倉庫的尺寸遠(yuǎn)超出預(yù)計(jì),可以用下面的方法找到并移除 大 (尺寸) 對(duì)象。
警告:此方法會(huì)破壞提交歷史。為了移除對(duì)一個(gè)大文件的引用,從最早包含該引用的 tree 對(duì)象開始之后的所有 commit 對(duì)象都會(huì)被重寫。如果在剛導(dǎo)入一個(gè)倉庫并在其他人在此基礎(chǔ)上開始工作之前這么做,那沒有什么問題 ── 否則你不得不通知所有協(xié)作者 (貢獻(xiàn)者) 去衍合你新修改的 commit 。
為了演示這點(diǎn),往 test 倉庫中加入一個(gè)大文件,然后在下次提交時(shí)將它刪除,接著找到并將這個(gè)文件從倉庫中永久刪除。首先,加一個(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 tarball1 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 tarball1 files changed, 0 insertions(+), 0 deletions(-)delete mode 100644 git.tbz2 對(duì)倉庫進(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í)刪除文件并沒有真正將其從歷史記錄中刪除。每當(dāng)有人復(fù)制這個(gè)倉庫去取得這個(gè)小項(xiàng)目時(shí),都不得不復(fù)制所有 2MB 數(shù)據(jù),而這僅僅因?yàn)槟阍?jīng)不小心加了個(gè)大文件。當(dāng)我們來解決這個(gè)問題。
首先要找出這個(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)簡單使用過的?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 接下來要將該文件從歷史記錄的所有 tree 中移除。很容易找出哪些 commit 修改了這個(gè)文件:
$ git log --pretty=oneline -- git.tbz2
da3f30d019005479c99eb4c3406225613985a1db oops - removed large tarball
6df764092f3e7c8f5f94cbe08ee5cf42e92a0289 added git tarball 必須重寫從?6df76?開始的所有 commit 才能將文件從 Git 歷史中完全移除。這么做需要用到第 6 章中用過的?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?命令來刪除一個(gè)特定文件,而是必須用?git rm --cached?來刪除它 ── 即從索引而不是磁盤刪除它。這樣做是出于速度考慮 ── 由于 Git 在運(yùn)行你的 filter 之前無需將所有版本簽出到磁盤上,這個(gè)操作會(huì)快得多。也可以用--tree-filter?來完成相同的操作。git rm?的?--ignore-unmatch?選項(xiàng)指定當(dāng)你試圖刪除的內(nèi)容并不存在時(shí)不顯示錯(cuò)誤。最后,因?yàn)槟闱宄栴}是從哪個(gè) commit 開始的,使用filter-branch?重寫自?6df7640?這個(gè) commit 開始的所有歷史記錄。不這么做的話會(huì)重寫所有歷史記錄,花費(fèi)不必要的更多時(shí)間。
現(xiàn)在歷史記錄中已經(jīng)不包含對(duì)那個(gè)文件的引用了。不過 reflog 以及運(yùn)行?filter-branch?時(shí) Git 往?.git/refs/original?添加的一些 refs 中仍有對(duì)它的引用,因此需要將這些引用刪除并對(duì)倉庫進(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 后倉庫的大小減小到了 7K ,遠(yuǎn)小于之前的 2MB 。從 size 值可以看出大文件對(duì)象還在松散對(duì)象中,其實(shí)并沒有消失,不過這沒有關(guān)系,重要的是在再進(jìn)行推送或復(fù)制,這個(gè)對(duì)象不會(huì)再傳送出去。如果真的要完全把這個(gè)對(duì)象刪除,可以運(yùn)行git prune --expire?命令。
?
9.8? 總結(jié)
現(xiàn)在你應(yīng)該對(duì) Git 可以作什么相當(dāng)了解了,并且在一定程度上也知道了 Git 是如何實(shí)現(xiàn)的。本章覆蓋了許多 plumbing 命令 ── 這些命令比較底層,且比你在本書其他部分學(xué)到的 porcelain 命令要來得簡單。從底層了解 Git 的工作原理可以幫助你更好地理解為何 Git 實(shí)現(xiàn)了目前的這些功能,也使你能夠針對(duì)你的工作流寫出自己的工具和腳本。
Git 作為一套 content-addressable 的文件系統(tǒng),是一個(gè)非常強(qiáng)大的工具,而不僅僅只是一個(gè) VCS 供人使用。希望借助于你新學(xué)到的 Git 內(nèi)部原理的知識(shí),你可以實(shí)現(xiàn)自己的有趣的應(yīng)用,并以更高級(jí)便利的方式使用 Git。
?
轉(zhuǎn)載于:https://www.cnblogs.com/yhaing/p/8473553.html
總結(jié)
以上是生活随笔為你收集整理的Git详解之九 Git内部原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 电影《使徒行者2》如何?
- 下一篇: redis使用epoll