15分鐘成為 GIT 專家

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-objectmktee 命令直接向倉(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 treelatest 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.txtf2.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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末基显,一起剝皮案震驚了整個(gè)濱河市蘸吓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌撩幽,老刑警劉巖美澳,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異摸航,居然都是意外死亡制跟,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門酱虎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)雨膨,“玉大人,你說(shuō)我怎么就攤上這事读串×募牵” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵恢暖,是天一觀的道長(zhǎng)排监。 經(jīng)常有香客問(wèn)我,道長(zhǎng)杰捂,這世上最難降的妖魔是什么舆床? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮嫁佳,結(jié)果婚禮上挨队,老公的妹妹穿的比我還像新娘。我一直安慰自己蒿往,他們只是感情好盛垦,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著瓤漏,像睡著了一般腾夯。 火紅的嫁衣襯著肌膚如雪颊埃。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,185評(píng)論 1 284
  • 那天蝶俱,我揣著相機(jī)與錄音班利,去河邊找鬼。 笑死跷乐,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的趾浅。 我是一名探鬼主播愕提,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼皿哨!你這毒婦竟也來(lái)了浅侨?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤证膨,失蹤者是張志新(化名)和其女友劉穎如输,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體央勒,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡不见,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了崔步。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片稳吮。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖井濒,靈堂內(nèi)的尸體忽然破棺而出灶似,到底是詐尸還是另有隱情,我是刑警寧澤瑞你,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布酪惭,位于F島的核電站,受9級(jí)特大地震影響者甲,放射性物質(zhì)發(fā)生泄漏春感。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一虏缸、第九天 我趴在偏房一處隱蔽的房頂上張望甥厦。 院中可真熱鬧,春花似錦寇钉、人聲如沸刀疙。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)谦秧。三九已至竟纳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間疚鲤,已是汗流浹背锥累。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留集歇,地道東北人桶略。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像诲宇,于是被迫代替她去往敵國(guó)和親际歼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容

  • 本片內(nèi)容轉(zhuǎn)自CSDN http://blog.csdn.net/ithomer/article/details/7...
    五娃兒閱讀 4,917評(píng)論 2 88
  • 進(jìn)入體制內(nèi)姑蓝,我學(xué)到的第一句話就是同事是沒(méi)辦法選擇的鹅心,這里不比公司,今天和這個(gè)同事共事纺荧,明天和那個(gè)同事共事旭愧,后天你自...
    90后螺絲釘閱讀 2,481評(píng)論 1 6
  • 本文參與#漫步青春#征文活動(dòng),作者:陳雅婷宙暇,本人承諾输枯,文章內(nèi)容為原創(chuàng),且未在其他平臺(tái)發(fā)布 為你駐足 雪 滿天飛 而...
    久厥閱讀 253評(píng)論 0 0
  • 簡(jiǎn)單介紹使用 python manage.py shell 查看數(shù)據(jù)庫(kù)中某個(gè)表中的數(shù)據(jù)占贫,也可以說(shuō)查看某個(gè)模型對(duì)應(yīng)的...
    chenyu1520閱讀 11,966評(píng)論 0 3
  • 大自然的交響曲 作者 小刨 11歲 在一個(gè)風(fēng)雨交加的夜晚用押, 你是否和我一樣靜靜地縮在床上? 看著那窗外噼里啪啦的...
    xingfudehouse閱讀 194評(píng)論 2 1