15分鐘成為 GIT 專家
通過(guò)一步一步的實(shí)踐來(lái)探索 git 內(nèi)部侨舆。
Git 可能看起來(lái)像一個(gè)復(fù)雜的系統(tǒng)脱货。如果上 Googl e搜索颂跨。Google 會(huì)自動(dòng)彈出一些最常搜索的標(biāo)題:
為什么 Git 這么難祈惶。柒瓣。诱告。
Git 就是太難了撵枢。。精居。
我們能夠停止假裝 Git 很簡(jiǎn)單锄禽、很容易學(xué)習(xí)嗎。靴姿。沃但。
為什么 Git 如此復(fù)雜。佛吓。宵晚。
乍一看,這些問(wèn)題好像都是真的维雇,但是你一旦理解了內(nèi)部的概念淤刃,使用 Git 工作會(huì)變成一件愉悅的體驗(yàn)。Git 的問(wèn)題是它非常靈活吱型。所有靈活的系統(tǒng)的特點(diǎn)就是復(fù)雜逸贾。我強(qiáng)烈的認(rèn)為解決其復(fù)雜性的唯一辦法就是深入它提供的用戶接口下面,理解內(nèi)部的模型和架構(gòu)唁影。一旦你這么做了,就不會(huì)有什么魔力和非預(yù)期的結(jié)果掂名。使用起這些復(fù)雜的工具得心應(yīng)手据沈。
不管是以前使用過(guò) Git 還是剛開(kāi)始使用這個(gè)神奇的版本控制工具的開(kāi)發(fā)者,閱讀了本文以后都會(huì)收獲頗豐饺蔑。如果你是應(yīng)一名有經(jīng)驗(yàn)的 GIT 使用者锌介,你會(huì)更好的理解 checkout -> modify -> commit
這個(gè)過(guò)程。如果你剛開(kāi)始使用 Git猾警,本文將給你一個(gè)很好的開(kāi)端孔祸。
在本文中我將使用一些底層的命令來(lái)展示 Git 內(nèi)部是怎么工作的。你不需要記住這些命令发皿,因?yàn)樵诔R?guī)的工作流中幾乎不會(huì)使用這些命令崔慧,但是這些命令在解釋 Git 內(nèi)部架構(gòu)時(shí)不可或缺。
本文比較長(zhǎng)穴墅,我相信你會(huì)按照以下兩種方式閱讀:
- 快速?gòu)捻敳炕撞炕淌遥匆幌卤疚牡哪夸洏?biāo)題
- 跟著本文的練習(xí)完整閱讀本文
通過(guò)練習(xí)你可以增強(qiáng)在這里獲得的信息温自。
Git 是一個(gè)文件夾
當(dāng)你在一個(gè)文件夾中執(zhí)行 git init
命令時(shí),Git 會(huì)創(chuàng)建 .git
目錄皇钞。所以我們打開(kāi)一個(gè)終端悼泌,創(chuàng)建一個(gè)新的目錄并在這里初始化一個(gè)空的 git 倉(cāng)庫(kù):
$ mkdir git-playground && cd git-playground
$ git init
Initialized empty Git repository in path/to/git-playground/.git/
$ ls .git
HEAD config description hooks info objects refs
這是 Git 存儲(chǔ)所有 commit 和其他用于操作這些 commit 相關(guān)信息的地方。當(dāng)你克隆一個(gè)倉(cāng)庫(kù)的時(shí)候就是復(fù)制這個(gè)目錄到你的文件夾夹界,為倉(cāng)庫(kù)里的每一個(gè)分支創(chuàng)建一個(gè)遠(yuǎn)程跟蹤分支馆里,并根據(jù) HEAD 文件檢出一個(gè)初始的分支。我們將在稍后討論在 Git 架構(gòu)中 HEAD 文件的用途可柿,但是這里需要記住的就是克隆一個(gè)倉(cāng)庫(kù)本質(zhì)上就是僅僅從別的地方復(fù)制一份 .git
目錄鸠踪。
Git 是一個(gè)數(shù)據(jù)庫(kù)
Git 是一個(gè)簡(jiǎn)單的 key-value 數(shù)據(jù)倉(cāng)庫(kù)。你可以將數(shù)據(jù)存儲(chǔ)到倉(cāng)庫(kù)中并獲得一個(gè)鍵值趾痘,通過(guò)這個(gè)鍵值你可以訪問(wèn)存儲(chǔ)的數(shù)據(jù)慢哈。將數(shù)據(jù)存儲(chǔ)到數(shù)據(jù)庫(kù)的命令是 hash-object
,這個(gè)命令會(huì)返回一個(gè)40個(gè)字符的哈希校驗(yàn)和永票,這個(gè)校驗(yàn)和會(huì)被用作鍵值卵贱。這個(gè)命令會(huì)在 git 倉(cāng)庫(kù)中創(chuàng)建一個(gè)稱為 blob
的對(duì)象。我們向數(shù)據(jù)庫(kù)中寫入一個(gè)簡(jiǎn)單的字符串 f1 content
:
$ F1CONTENT_BLOB_HASH=$( \
echo 'f1 content' | git hash-object -w --stdin )
$ echo $F1CONTENT_BLOB_HASH
a1deaae8f9ac984a5bfd0e8eecfbafaf4a90a3d0
如果你對(duì) shell 不熟悉侣集,上面這一段代碼的主要命令是:
echo 'f1 content' | git hash-object -w --stdin
echo
命令輸出 f1 content
字符串键俱,通過(guò)管道操作符 |
我們將輸出重定位到 git hash-object
命令。hash-object
的參數(shù) -w
表示要存儲(chǔ)這個(gè)對(duì)象世分;否則這個(gè)命令只是簡(jiǎn)單的告訴你鍵值是什么编振。 --stdin
告訴命令從 stdin 讀取內(nèi)容;如果不指定這一點(diǎn)臭埋, hash-object
希望最后輸入一個(gè)文件路徑踪央。前面已經(jīng)說(shuō)到 git hash-object
命令會(huì)返回一個(gè)哈希值,我將這個(gè)值存儲(chǔ)到 F1CONTENT_BLOB_HASH
變量中瓢阴。我們也可以將主命令和變量賦值像這樣分開(kāi):
$ echo 'f1 content' | git hash-object -w --stdin
a1deaae8f9ac984a5bfd0e8eecfbafaf4a90a3d0
$ F1CONTENT_BLOB_HASH=a1deaae8f9ac984a5bfd0e8eecfbafaf4a90a3d0
但是為了方便畅蹂,我將在后面的代碼中使用簡(jiǎn)短的版本為變量賦值。這些變量會(huì)在需要哈希字符串的地方使用荣恐,它和 $
符號(hào)拼接起來(lái)作為一個(gè)變量讀取存儲(chǔ)的數(shù)據(jù)液斜。
通過(guò)鍵值讀取數(shù)據(jù)可以使用 帶有 -p
選項(xiàng)的 cat-file
命令。這個(gè)命令需要接收帶讀取數(shù)據(jù)的哈希值:
如我前面所說(shuō)叠穆, .git
是一個(gè)文件夾少漆,并且所有存儲(chǔ)的值/對(duì)象都放在這個(gè)文件夾中。所以我們可以瀏覽一下 .git/objects
文件夾硼被,你會(huì)看到 Git 創(chuàng)建了一個(gè)名稱為 a1
的文件夾示损,這是哈希值的前兩個(gè)字母:
$ ls .git/objects/ -l
**a1/**
info/
pack/
這就是 Git 存儲(chǔ)對(duì)象的方式--每個(gè) blob 一個(gè)文件夾。然而嚷硫,Git 也可以將多個(gè) blob 合并成一個(gè)文件生成一個(gè) pack 文件屎媳,這些 pack 文件就存儲(chǔ)在你前面看到的 pack
目錄夺溢。Git 將這些 pack 對(duì)象相關(guān)的信息都存儲(chǔ)到 info
目錄。Git 基于 blob 的內(nèi)容為每一個(gè) blob 生成哈希值烛谊,所以存儲(chǔ)在 Git 中的對(duì)象是不可修改的风响,因?yàn)樾薷膬?nèi)容就會(huì)改變哈希值。
我們往倉(cāng)庫(kù)中寫入另外一個(gè)字符串 f2 content
:
$ F2CONTENT_BLOB_HASH=$( \
**echo 'f2 content' | git hash-object -w --stdin )**
如你所預(yù)期的那樣丹禀,你會(huì)看到 .git/objects/
目錄下現(xiàn)在有兩條記錄 9b/
和 a1/
:
$ ls .git/objects/ -l
**9b/**
**a1/ **
info/
pack/
樹(shù)(Tree)是一個(gè)內(nèi)部組件
現(xiàn)在我們的倉(cāng)庫(kù)中有兩個(gè)blob:
F1CONTENT_BLOB_HASH -> ‘f1 content’
F2CONTENT_BLOB_HASH -> ‘f2 content'
我們需要一種方式來(lái)將他們組織到一起状勤,并且將每一個(gè) blob 和一個(gè)文件名關(guān)聯(lián)起來(lái)。這就是 tree 的作用双泪。我們可以按照下面的語(yǔ)法通過(guò) git mktree
為從而每一個(gè) blob/文件 關(guān)聯(lián)創(chuàng)建一個(gè)樹(shù):
[file-mode object-type object-hash file-name]
關(guān)于文件的 file mode 可以參考這個(gè)答案提供的解釋持搜。我們將使用 100644
模式,這一模式下 blob 就是一個(gè)常規(guī)文件每一個(gè)用戶都可以讀寫焙矛。當(dāng)檢出文件到工作目錄時(shí)葫盼,Git 會(huì)根據(jù) tree 實(shí)體將相應(yīng)的文件/目錄設(shè)置成這個(gè)模式。
所以村斟,這樣就可以將兩個(gè) blob 和兩個(gè)文件建立關(guān)聯(lián):
$ INITIAL_TREE_HASH=$( \
printf '%s %s %s\t%s\n' \
100644 blob $F1CONTENT_BLOB_HASH f1.txt \
100644 blob $F2CONTENT_BLOB_HASH f2.txt |
git mktree )
和 hash-object
一樣贫导,mktree
命令也會(huì)返回創(chuàng)建好的樹(shù)對(duì)象的哈希值:
$ echo $INITIAL_TREE_HASH
e05d9daa03229f7a7f6456d3d091d0e685e6a9db
所以,現(xiàn)在我們的倉(cāng)庫(kù)中有這樣一個(gè)樹(shù):
運(yùn)行這個(gè)命令之后蟆盹,git 在倉(cāng)庫(kù)中創(chuàng)建了第三個(gè) tree
類型的對(duì)象孩灯。我們一起來(lái)看看:
$ ls .git/objects -l
e0 <--- initial tree object (INITIAL_TREE_HASH)
9b <--- 'f1 content' blob (F2CONTENT_BLOB_HASH)
a1 <--- 'f2 content' blob (F2CONTENT_BLOB_HASH)
當(dāng)使用 mktree
命令的時(shí)候,我們也可以指定另外一個(gè)樹(shù)對(duì)象(而不是一個(gè) blob)作為參數(shù)逾滥。新創(chuàng)建的樹(shù)會(huì)和目錄而不是一個(gè)常規(guī)文件關(guān)聯(lián)峰档。例如,下面的命令會(huì)根據(jù)一個(gè) subtree 創(chuàng)建一個(gè)和 nested-folder
目錄關(guān)聯(lián)的樹(shù):
printf ‘%s %s %s\t%s\n’ 040000 tree e05d9da nested-folder | git mktree
文件模式 040000
表明是一個(gè)目錄寨昙,并且我們使用的類型 tree
而不是 blob
讥巡。這就是 git 在項(xiàng)目結(jié)構(gòu)中存儲(chǔ)嵌套目錄的方式。
Index 是安裝樹(shù)的地方
每一個(gè)使用 GIT 工作的人都應(yīng)該很熟悉 index 或者 staging 區(qū)這兩個(gè)概念舔哪,并且可能看到過(guò)這張圖片:
在右側(cè)你可以看到 git repository欢顷,它用于存儲(chǔ) git 對(duì)象:blobs,trees尸红,commits 和 tags吱涉。我們已經(jīng)使用 hash-object
和 mktee
命令直接向倉(cāng)庫(kù)中添加了兩個(gè) blob 和一個(gè)樹(shù)對(duì)象到倉(cāng)庫(kù)中刹泄。左側(cè)的工作目錄是你本地的文件系統(tǒng)(目錄)外里,也就是你檢出所有項(xiàng)目文件的地方。中間這個(gè)區(qū)域我們稱為 index 文件或者簡(jiǎn)稱 index特石。它是一個(gè)二進(jìn)制文件(通常存儲(chǔ)在 .git/index
)盅蝗,類似于樹(shù)對(duì)象的結(jié)構(gòu)。它持有一個(gè)排序好的文件路徑列表姆蘸,每一個(gè)文件路徑都有權(quán)限以及 blob/tree 對(duì)象的 SHA1 值墩莫。
在這個(gè)地方芙委,git 在作如下操作之前準(zhǔn)備一個(gè)樹(shù):
- 將一個(gè)樹(shù)寫入倉(cāng)庫(kù),或者
- 將一個(gè)樹(shù)檢出到工作目錄
現(xiàn)在我們的倉(cāng)庫(kù)中已經(jīng)有一個(gè)在上一章節(jié)創(chuàng)建的樹(shù)狂秦。我們現(xiàn)在可以使用 read-tree
命令將這個(gè)樹(shù)從倉(cāng)庫(kù)中讀取到 index 文件:
$ git read-tree $INITIAL_TREE_HASH
所以現(xiàn)在我們期望 index 文件中有兩個(gè)文件灌侣。我們可以使用 git ls-files -s
命令來(lái)檢查當(dāng)前 index 文件的結(jié)構(gòu):
$ git ls-files -s
100644 a1deaae8f9ac984a5bfd0e8eecfbafaf4a90a3d0 0 f1.txt
100644 9b96e21cb748285ebec53daec4afb2bdcb9a360a 0 f2.txt
由于我們還沒(méi)有對(duì) index 文件做任何修改,它和我們用于生成index文件的樹(shù)完全一致裂问。一旦我們?cè)?index 文件中有了正確的結(jié)構(gòu)侧啼,我們就可以通過(guò)帶有 -a
選項(xiàng)的 checkout-index
命令將它檢出到工作目錄:
$ git checkout-index -a
$ ls
f1.txt f2.txt
$ cat f1.txt
f1 content
$ cat f2.txt
f2 content
對(duì)的!我們已經(jīng)將沒(méi)使用任何 commit 就添加到 git 倉(cāng)庫(kù)中的內(nèi)容檢出了堪簿。是不是很酷痊乾?
但是 index 文件并非總是停留在初始樹(shù)的狀態(tài)。你可能知道它可以通過(guò)這些命令改變椭更,git add [file path]
和 git rm --cached [file path]
處理單個(gè)文件哪审,git add .
和 git reset
處理一批已修改/已刪除的文件。我們將這個(gè)知識(shí)用于實(shí)踐虑瀑,在倉(cāng)庫(kù)中創(chuàng)建一個(gè)新的樹(shù)湿滓,這個(gè)樹(shù)包含一個(gè)和文本文件 f3.txt
關(guān)聯(lián)的 blob 文件。文件的內(nèi)容就是字符串 f3 content
缴川。但是和前一節(jié)手動(dòng)創(chuàng)建樹(shù)不一樣茉稠,我們將使用index文件來(lái)創(chuàng)建。
現(xiàn)在我們的 index 文件結(jié)構(gòu)如下把夸,
這就是我們應(yīng)用修改的基準(zhǔn)而线。你對(duì) index 文件所做的所有修改在將樹(shù)寫入倉(cāng)庫(kù)之前都是暫時(shí)的。然而你添加的對(duì)象是立刻寫入到倉(cāng)庫(kù)的恋日。如果你放棄當(dāng)前對(duì)樹(shù)的修改膀篮,這些對(duì)象稍后會(huì)被垃圾回收搜集并刪除。 這意味著如果你不小心丟棄了對(duì)某一個(gè)文件的修改岂膳,在 git 運(yùn)行 GC 之前是可以恢復(fù)的誓竿。垃圾回收通常發(fā)生在有太多的未引用對(duì)象時(shí)才發(fā)生。
我們來(lái)刪除工作目錄中的兩個(gè)文件:
$ rm f1.txt f2.txt
如果我們運(yùn)行git status
我們會(huì)看到以下信息:
$ git status
On branch master
Initial commit
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: f1.txt
new file: f2.txt
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
deleted: f1.txt
deleted: f2.txt
信息有點(diǎn)多谈截。有兩個(gè)文件被刪除筷屡、兩個(gè)新文件同時(shí)還是 “Initial commit”。我們來(lái)看看為什么簸喂。當(dāng)你運(yùn)行 git status
時(shí)毙死,git做了兩個(gè)比較:
- 將 index 文件和當(dāng)前的工作目錄比較 --變化是 “not staged for commit”
- 將 index 文件和 HEAD 提交比較 --變化是 “to be committed”
所以在這里我們看到 git 將兩個(gè)已刪除的文件報(bào)告為 “Changes not staged for commit”,我們已經(jīng)知道這個(gè)信息是怎產(chǎn)生的--它將當(dāng)前的工作目錄和 index 文件比較發(fā)現(xiàn)工作目錄丟失兩個(gè)文件(因?yàn)槲覀儎偛艅h除了)喻鳄。
我們同時(shí)還看在 “Changes to be committed” 下面 git 報(bào)告了了兩個(gè)新文件扼倘。這是因?yàn)榈侥壳盀橹刮覀兊膫}(cāng)庫(kù)中還沒(méi)有任何提交,所以這個(gè) HEAD
文件(我們稍后做詳細(xì)的解釋)指向一個(gè)所謂的“空樹(shù)”對(duì)象(沒(méi)有任何文件)除呵。所以 Git 以為我們剛剛創(chuàng)建了一個(gè)新的倉(cāng)庫(kù)再菊,所以為什么它顯示 “Initial commit”爪喘,并將 index 文件中的所有文件都當(dāng)做新文件惊奇。
現(xiàn)在如果我們執(zhí)行 git add .
它將修改 index 文件(刪除了兩個(gè)文件)了牛,然后再次執(zhí)行 git status
就會(huì)顯示沒(méi)有任何修改陨溅,因?yàn)楝F(xiàn)在我們的工作目錄和 index 文件中都沒(méi)有文件:
$ git add .
$ git status
On branch master
Initial commit
nothing to commit (create/copy files and use "git add" to track)
我們繼續(xù)通過(guò)創(chuàng)建新文件 f3.txt
來(lái)創(chuàng)建一個(gè)新的樹(shù)悬荣。
$ echo ‘f3 content’ > f3.txt
$ git add f3.txt
如果現(xiàn)在運(yùn)行 git status
:
$ git status
On branch master
Initial commit
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: f3.txt
我們發(fā)現(xiàn)檢查到了一個(gè)新文件寄雀。同樣弥姻,這個(gè)修改是報(bào)告在 "Changes to be committed" 下银亲,所以現(xiàn)在 Git 是將 index 文件和 “空樹(shù)” 作比較变姨。所以認(rèn)為 index 文件中已經(jīng)有了新的文件 blob吕粹。我們來(lái)確認(rèn)一下:
$ git ls-files -s
100644 5927d85c2470d49403f56ce27afd8f74b1a42589 0 f3.txt
# Save the hash of the f3.txt file blob
$ F3CONTENT_BLOB_HASH=5927d85c2470d49403f56ce27afd8f74b1a42589
好了种柑,index 的結(jié)構(gòu)是正確的,我們現(xiàn)在可以通過(guò)這個(gè) index 在倉(cāng)庫(kù)中創(chuàng)建一個(gè)樹(shù)匹耕。我們通過(guò) write-tree
命令來(lái)完成:
$ LATEST_TREE_HASH=$( git write-tree )
很棒聚请。我們剛才通過(guò) index 創(chuàng)建了一個(gè)樹(shù)。并且將新的樹(shù)的哈希值存到了 LATEST_TREE_HASH
變量稳其。我們已經(jīng)通過(guò)手動(dòng)將 f3 content
blob 寫入到倉(cāng)庫(kù)并且通過(guò) mktree
來(lái)創(chuàng)建了一個(gè)樹(shù)驶赏,但是使用 index 文件更方便。
有趣的是如果你現(xiàn)在運(yùn)行 git status
你會(huì)發(fā)現(xiàn)git 仍然認(rèn)為存在一個(gè)新文件 f3.txt
:
$ git status
On branch master
Initial commit
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: f3.txt
那是因?yàn)楸M管我們已經(jīng)創(chuàng)建了一個(gè)樹(shù)并將它存入了倉(cāng)庫(kù)既鞠,但是我們還沒(méi)有更新用于比較的 HEAD 文件煤傍。
所以加上我們新創(chuàng)建的樹(shù),倉(cāng)庫(kù)中有以下對(duì)象:
Commit就是對(duì)樹(shù)的一次封裝
在這一節(jié)中將變得更有趣嘱蛋。在我們?nèi)粘5?Git 使用中蚯姆,我們基本不會(huì)使用樹(shù)或者 blob。我們和 commit 對(duì)象交互洒敏。所以 git 中的 commit 是什么龄恋?實(shí)際上,簡(jiǎn)單說(shuō)它就是對(duì)樹(shù)對(duì)象的封裝:
- 允許給一個(gè)樹(shù)(一組文件)添加消息
- 允許指定父 commit
現(xiàn)在我們的倉(cāng)庫(kù)中有兩個(gè)樹(shù)--initial tree
和 latest tree
凶伙。我們通過(guò) commit-tree
命令將第一個(gè)樹(shù)封裝成一個(gè) commit(將樹(shù)的哈希值傳遞給它):
INITIAL_COMMIT_HASH=$( \
echo 'initial commit' | git commit-tree $INITIAL_TREE_HASH )
在運(yùn)行上面的命令之后:
現(xiàn)在我么可以將這個(gè)commit檢出到工作目錄:
$ git checkout $INITIAL_COMMIT_HASH
A f3.txt
HEAD is now at a27a75a... initial commit
我們現(xiàn)在可以看到 f1.txt f2.txt 處于工作目錄中:
$ ls
f1.txt f2.txt
$ cat f1.txt
f1 content
$ cat f2.txt
f2 content
當(dāng)你運(yùn)行 git checkout [commit-hash]
時(shí)郭毕,git 做了如下動(dòng)作:
- 將 commit 點(diǎn)的樹(shù)讀入到 index 文件
- 將 index 文件檢出到工作目錄
- 使用 commit 的哈希值更新 HEAD 文件
這些都是我們?cè)谏弦还?jié)手動(dòng)執(zhí)行的操作。
Git歷史就是一串commit
所以現(xiàn)在我們知道了一個(gè) commit 就是對(duì)一個(gè)樹(shù)的封裝函荣。我也講到一個(gè) commit 可以有一個(gè)父 commit显押。我們最初有兩個(gè)樹(shù)并在上一節(jié)將其中一個(gè)封裝成了一個(gè)commit,所以現(xiàn)在我們還有一個(gè)孤立的樹(shù)傻挂。我們來(lái)將它封裝成另外一個(gè) commit 并指定其父 commit 為 initial commit乘碑。我們會(huì)使用和前一節(jié)相同的操作 commit-tree
,不過(guò)需要通過(guò)-p
選項(xiàng)來(lái)指定父 commit踊谋。
$ LATEST_COMMIT_HASH=$( \
echo 'latest commit' |
git commit-tree $LATEST_TREE_HASH -p $INITIAL_COMMIT_HASH )
現(xiàn)在應(yīng)該是這樣:
所以如果你現(xiàn)在將最后一次 commit 的哈希值傳遞給 git log
你會(huì)看到提交歷史中有兩條提交記錄:
$ git log --pretty=oneline $LATEST_COMMIT_HASH
[some hash] latest commit
[some hash] initial commit
并且你可以在他們之間切換蝉仇。這里是 initial commit:
$ git checkout $INITIAL_COMMIT_HASH
$ ls
f1.txt f2.txt
latest commit
$ git checkout $LATEST_COMMIT_HASH
$ ls
f3.txt
HEAD 是對(duì)已檢出的 commit 的引用
HEAD 是存放在 .git/HEAD
的文本文件旋讹,它是對(duì)當(dāng)前已檢出 commit 的引用殖蚕。由于我們?cè)谇懊嬉还?jié)中通過(guò) $LATEST_COMMIT_HASH
檢出了最后的commit轿衔,此時(shí) HEAD
文件包含的全部?jī)?nèi)容:
$ cat .git/HEAD
88d3b9901d62fc1de9219f388e700d98bdb97ba9
$ [ $LATEST_COMMIT_HASH == "88d3b9901d62..." ]; echo 'equal'
equal
然而,通常 HEAD 文件是通過(guò)分支引用來(lái)引用當(dāng)前檢出的 commit睦疫。當(dāng)它直接引用一個(gè) commit 的時(shí)候它是處于 detached state
(分離狀態(tài))害驹。但是即使當(dāng) HEAD 像這樣通過(guò)分支持有一個(gè)引用:
ref: refs/heads/master
它仍然是引用一個(gè) commit 的哈希值。
你現(xiàn)在知道了在執(zhí)行 git status
命令時(shí)蛤育, Git 使用通過(guò)HEAD
引用的 commit 來(lái)產(chǎn)生一系列 index 文件和當(dāng)前檢出的樹(shù)/commit 之間的修改宛官。HEAD
的另外一個(gè)用途就是決定下一個(gè) commit 的父 commit。
有趣的是瓦糕,HEAD 文件對(duì)大多數(shù)操作都是如此重要以至于如果你手動(dòng)清除其內(nèi)容底洗,Git 將認(rèn)為不是一個(gè) git 倉(cāng)庫(kù)并報(bào)錯(cuò):
fatal: Not a git repository (or any of the parent directories): .git
分支是一個(gè)指向某一個(gè)commit的文本文件
所以現(xiàn)在我們的倉(cāng)庫(kù)中有兩條 commit,形成了如下提交歷史:
$ git log --pretty=oneline $LATEST_COMMIT_HASH
[some hash] latest commit
[some hash] initial commit
我們?cè)谝延械臍v史中引入一個(gè)分叉咕娄。我們將檢出最初的 commit 并修改 f1.txt
文件內(nèi)容亥揖。然后使用你已經(jīng)習(xí)慣的 git commit
命令創(chuàng)建一條新的 commit:
$ git checkout $INITIAL_COMMIT_HASH
$ echo 'I am modified f1 content' > f1.txt
$ git add f1.txt
$ git commit -m "forked commit"
1 file changed, 1 insertion(+), 1 deletion(-)
以上的代碼片段:
- 檢出
"initial commit"
將f1.txt
和f2.txt
添加到工作目錄 - 將
f1.txt
的內(nèi)容也替換為字符串I am modified f1 content
- 使用
git add
更新index 文件
最后這個(gè)我們熟悉的git commit
命令內(nèi)部做了以下操作: - 從 index 文件創(chuàng)建一個(gè)樹(shù)
- 將樹(shù)寫入倉(cāng)庫(kù)
- 創(chuàng)建一個(gè) commit 對(duì)象將樹(shù)封裝起來(lái)
- 將
initial commit
作為新創(chuàng)建 commit 的父commit,因?yàn)楫?dāng)前HEAD
文件中的 commit 就是initial commit
圣勒。
我們同樣需要將新的 commit 的哈希值存儲(chǔ)到變量中费变。由于 Git 根據(jù)當(dāng)前的 commit 文件更新 HEAD,我們可以這樣讀取這個(gè)值:
FORKED_COMMIT_HASH=$( cat .git/HEAD )
所以現(xiàn)在我們的 git 倉(cāng)庫(kù)中是這樣一些對(duì)象的:
由此生成以下提交歷史:
由于分叉的出現(xiàn)我們現(xiàn)在有兩條工作線圣贸。這意味著我們需要引入兩條分支獨(dú)立跟蹤每一條工作線挚歧。我們創(chuàng)建 master
分支來(lái)跟蹤從 latest commit
以來(lái)的直線歷史,創(chuàng)建 forked
分支來(lái)跟蹤自 forked commit
以來(lái)的歷史吁峻。
一個(gè)分支就是一個(gè)文本文件滑负,它包含了一個(gè)commit的哈希值。它是 git引用的一部分--引用一個(gè) commit 的一組對(duì)象用含。另外一個(gè)引用類型是輕量的 tag橙困。Git 將所有的引用存儲(chǔ)到 .git/refs
目錄,將所有分支存儲(chǔ)在 .git/refs/heads
目錄耕餐。由于分支就是一個(gè)文本文件凡傅,我們可以使用 commit 的哈希值來(lái)創(chuàng)建一個(gè)分支。
所以下面的分支將指向主分支的 "latest commit"肠缔。
$ echo $LATEST_COMMIT_HASH > .git/refs/heads/master
這一個(gè)分支將指向 "forked" 分支的 "forked commit":
$ echo $FORKED_COMMIT_HASH > .git/refs/heads/forked
所以最終我們回到了你常常使用的工作流---我們現(xiàn)在可以在分支之間切換:
$ git checkout master
Switched to branch 'master'
$ git log --pretty=oneline
[some hash] latest commit
[some hash] first commit
$ ls -l
f3.txt
一起來(lái)看看另外一個(gè) forked
分支:
$ git checkout forked
Switched to branch 'forked'
$ git log --pretty=oneline
f30305a8a23312f70ba985c8c644fcdca19dab95 forked commit
f30305a8a23312f70ba985c8c644fcdca19dab95 initial commit
$ git ls
f1.txt f2.txt
$ cat f1.txt
I am modified f1 content
一個(gè) tag 就是指向某一個(gè) commit 的文本文件
你興許已經(jīng)知道除了使用分支(一條工作線的)還可以使用 tag 來(lái)跟蹤單獨(dú)的 commit夏跷。Tag 通常用于標(biāo)記重要的開(kāi)發(fā)節(jié)點(diǎn)如版本發(fā)布。現(xiàn)在我們的倉(cāng)庫(kù)中有3個(gè) commit明未。我們可以使用 tag 來(lái)給它們命名槽华。和分支一樣,一個(gè) tag 就是一個(gè)文本文件趟妥,它包含了一個(gè) commit 的哈希值猫态,同樣也是引用組的一部分。
你已經(jīng)知道 git 將所有的引用都存儲(chǔ)在 .git/refs
目錄,所以tag都存儲(chǔ)在 .git/refs/tags
子目錄亲雪。由于它就是一個(gè)文本文件勇凭,我們可以創(chuàng)建一個(gè)文件并將 commit 的哈希值寫入其中。
所以這個(gè) tag 會(huì)指向 latest commit:
$ echo $FORKED_COMMIT_HASH > .git/refs/tags/forked
這個(gè) tag 會(huì)指向 initial commit:
$ echo $INITIAL_COMMIT_HASH > .git/refs/tags/initial
一旦完成了這一步我們就可以使用 tag 在 commit 之間切換义辕。這樣就可以切換到 initial commit:
$ git checkout tags/initial
HEAD is now at 285aec7... second commit
$ cat f1.txt
f1 content
這樣就切換到 forked commit:
$ git checkout tags/forked
$ cat f1.txt
I am modified f1 content
此外還有 "annotated-tag"虾标,它和我們現(xiàn)在使用的輕量級(jí) tag有所不同。它是一個(gè)對(duì)象灌砖,可以像commit一樣包含信息璧函,并且是其他對(duì)象一起存放在倉(cāng)庫(kù)中。
本文譯自Become a GIT pro by learning GIT architecture in 15 minutes