Git內(nèi)部原理

Git是一個(gè)快速,可擴(kuò)展的分布式版本控制系統(tǒng)。從根本上來說,Git是一個(gè)內(nèi)容尋址(content-addressable)文件系統(tǒng)。

Git提供了非常豐富的指令集,根據(jù)功能劃分為底層命令和高層命令,平時(shí)工作中使用最多的add、commit、reset等就屬于高層命令。大部分情況下我們不會(huì)接觸到Git的底層命令,但是為了了解Git的工作原理,了解底層命令就很重要了。

接下來我們來探索Git的內(nèi)部工作原理

.git目錄結(jié)構(gòu)

首先我們新建一個(gè)空文件夾,執(zhí)行git init初始化命令,創(chuàng)建git版本庫。

$ mkdir test1
$ cd test1
$ git init
$ cd .git
$ ls -F1

.git目錄結(jié)構(gòu)如下:
HEAD         // HEAD指針,指向當(dāng)前分支
branches/    // 分支
config       // 項(xiàng)目特有的配置選項(xiàng)
description  // 僅供 GitWeb 程序使用,無需關(guān)心
hooks/       // 客戶端或服務(wù)端的鉤子腳本
info/        
objects/     // 存儲(chǔ)所有數(shù)據(jù)內(nèi)容
refs/        // 存儲(chǔ)指向數(shù)據(jù)(分支)的提交對象的指針
index        // 保存暫存區(qū)信息(尚待創(chuàng)建)

.git的目錄結(jié)構(gòu)及作用上面已經(jīng)添加了備注,其中有四個(gè)條目很重要:HEAD文件、尚未創(chuàng)建的index文件,和objects目錄、refs目錄。這些條目是Git的核心組成部分。

Git對象

Git一共有四種類型的對象:

  • Blob object
  • Commit object
  • Tree object
  • Tag object

Blob對象存儲(chǔ):

  • 數(shù)據(jù)內(nèi)容(文本文件、源代碼、圖片等)

Commit對象存儲(chǔ):

  • tree對象
  • parent指針(如果有)
  • 作者對象(姓名、郵箱、提交時(shí)間)
  • 提交者對象(姓名、郵箱、提交時(shí)間)

Tree對象存儲(chǔ):

  • 指向數(shù)據(jù)內(nèi)容或者Tree對象的指針(SHA-1)
  • 模式
  • 類型
  • 文件名

Tag對象一般是Commit對象

Tree對象對應(yīng)文件目錄,blob對象對應(yīng)文件,commit對象對應(yīng)當(dāng)前分支的快照(snapshot)

執(zhí)行g(shù)it add和git commit時(shí)發(fā)生了什么?

上面已經(jīng)創(chuàng)建一個(gè)test1的空目錄,接下來我們添加一個(gè)文件到test1目錄。

echo 'hello' > test.txt

執(zhí)行g(shù)it status命令,可以看到目錄下多了個(gè)未跟蹤的文件test.txt,這時(shí)候執(zhí)行g(shù)it add命令,觀察.git目錄有什么變化。

git add test.txt
cd .git
→ tree
.
├── HEAD
├── branches
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── index
├── info
│   └── exclude
├── objects
│   ├── ce
│   │   └── 013625030ba8dba906f756967f9e9ca394464a
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

10 directories, 17 files

可以看到,執(zhí)行g(shù)it add命令后,.git/object/目錄下新增了一個(gè)目錄和一個(gè)文件(其實(shí)是一個(gè)40位哈希值,前兩位是目錄名,后38位是文件名),其實(shí)只是多出了一個(gè)blob對象,同時(shí)創(chuàng)建了一個(gè)index文件。后面我們會(huì)講解這個(gè)新增的blob對象以及index文件是如何生成的。

我們接著執(zhí)行g(shù)it commit命令,看看會(huì)發(fā)生什么。

→ tree
.
├── COMMIT_EDITMSG
├── HEAD
├── branches
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── index
├── info
│   └── exclude
├── logs
│   ├── HEAD
│   └── refs
│       └── heads
│           └── master
├── objects
│   ├── 2b
│   │   └── f705f913222f2032e114e609dfe7f2e97f23bd
│   ├── 92
│   │   └── 0512d27e4df0c79ca4a929bc5d4254b3d05c4c
│   ├── ce
│   │   └── 013625030ba8dba906f756967f9e9ca394464a
│   ├── info
│   └── pack
└── refs
    ├── heads
    │   └── master
    └── tags

15 directories, 23 files

觀察控制臺(tái)的輸出可以看到,commit之后多出了5個(gè)目錄和5個(gè)文件,我們只關(guān)注objects/目錄和refs/目錄。執(zhí)行commit之后,objects目錄下新增了兩個(gè)對象,同時(shí)refs/heads/目錄下新增了一個(gè)master文件。

總結(jié)一下,執(zhí)行g(shù)it add和git commit命令,objects目錄下新增了3個(gè)對象,新增了一個(gè)index文件和master文件。

這些對象和文件是如何產(chǎn)生的呢,下面我們通過git的底層命令來復(fù)盤上面的操作。

使用Git底層命令演示git add和git commit的原理

下面我們新建一個(gè)空目錄test2,并執(zhí)行g(shù)it init命令

mkdir test2
cd test2
git init

使用git has-object往Git數(shù)據(jù)庫寫入內(nèi)容

echo 'hello' | git hash-object -w --stdin
ce013625030ba8dba906f756967f9e9ca394464a

可以看到,控制臺(tái)輸出了一個(gè)40位的哈希值,與上面對比發(fā)現(xiàn)是一樣的。為什么是一樣的呢?因?yàn)镚it是通過頭部信息(header)+數(shù)據(jù)內(nèi)容(content)通過SHA-1檢驗(yàn)計(jì)算校驗(yàn)和生成的。

執(zhí)行完has-object命令后,觀察objects/目錄的變化,我們用find命令來查看

→ find .git/objects -type f
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a

可以看到object/目錄下多了一個(gè)對象,正是我們剛剛寫進(jìn)Git數(shù)據(jù)后返回的哈希值。

Git數(shù)據(jù)庫是一個(gè)簡單的key-value data store,哈希值作為key,數(shù)據(jù)內(nèi)容作為value。我們可以通過cat-file命令查看這個(gè)key對應(yīng)的內(nèi)容以及類型。

→ git cat-file -p ce013625030ba8dba906f756967f9e9ca394464a
hello
→ git cat-file -t ce013625030ba8dba906f756967f9e9ca394464a
blob

可以看到,輸出的內(nèi)容正是我們之前寫入的,類型是blob類型。

接著我們執(zhí)行g(shù)it status命令看看發(fā)生了什么

→ git status
On branch master

No commits yet

nothing to commit (create/copy files and use "git add" to track)

這里有一個(gè)問題,我們之前僅僅保存了文件內(nèi)容,并沒有指定文件名。我們使用tree命令,查看.git目錄,可以發(fā)現(xiàn),index文件還沒有被創(chuàng)建。

下面我們使用update-index命令來創(chuàng)建一個(gè)暫存區(qū),并為之前寫入的文件內(nèi)容指定一個(gè)文件名。

git update-index --add --cacheinfo 100644 ce013625030ba8dba906f756967f9e9ca394464a test.txt

再次使用tree命令查看

→ tree .git
.git
├── HEAD
├── branches
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── index
├── info
│   └── exclude
├── objects
│   ├── ce
│   │   └── 013625030ba8dba906f756967f9e9ca394464a
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

10 directories, 17 files

可以發(fā)現(xiàn)index文件被創(chuàng)建出來了。這個(gè)時(shí)候,再次執(zhí)行g(shù)it status

→ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

    new file:   test.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:    test.txt

可以發(fā)現(xiàn),test.txt文件已經(jīng)加入暫存區(qū)了,下面還有一個(gè)標(biāo)記為刪除的test.txt,為什么呢?
因?yàn)槲覀儽镜貨]有test.txt文件,而暫存區(qū)有這個(gè)文件,使用git status的時(shí)候是對比工作目錄和暫存區(qū)的文件。

如果介意這個(gè)deleted的log,可以已通過git checkout -- test.txt命令讓暫存區(qū)的文件覆蓋工作區(qū)的文件,我們先不管。

執(zhí)行g(shù)it add的時(shí)候,除了上面兩步,還有一步是將文件寫入一個(gè)樹對象??梢酝ㄟ^git write-tree命令來完成

git write-tree ce013625030ba8dba906f756967f9e9ca394464a
920512d27e4df0c79ca4a929bc5d4254b3d05c4c

執(zhí)行完后發(fā)現(xiàn),輸出了一個(gè)40位的哈希值,與上面git add之后對比,可以發(fā)現(xiàn)是同一個(gè)哈希值。
我們驗(yàn)證下這個(gè)對象的類型:

→ git cat-file -t 920512d27e4df0c79ca4a929bc5d4254b3d05c4c
tree

可以發(fā)現(xiàn)這確實(shí)是個(gè)樹對象。

至此,我們使用底層命令完成了git add所做的工作。git add的時(shí)候做什么,我們可以總結(jié)下:

  1. 將文件內(nèi)容寫入Git數(shù)據(jù)庫
  2. 更新暫存區(qū)并指定文件名,如果是首次提交到暫存區(qū)則是創(chuàng)建暫存區(qū)(對應(yīng).git目錄下的index文件)
  3. 將文件寫入樹對象

到這里我們還沒有提交對象,是時(shí)候創(chuàng)建一次提交了。我們可以使用commit-tree命令創(chuàng)建一次提交。

→ echo 'first commit' | git commit-tree 9205
c4cf32be0098f786df455e3fee67ba21779dd70a

可以發(fā)現(xiàn)這里輸出的哈希值與test1演示項(xiàng)目中的值不同,其他兩個(gè)都相同,為什么呢?因?yàn)閏ommit對象包含時(shí)間戳信息,所以計(jì)算出來的哈希值肯定是不一樣的。

驗(yàn)證下這個(gè)對象的類型:

→ git cat-file -t c4cf
commit

可以發(fā)現(xiàn)這確實(shí)是一個(gè)commit對象。

使用git log c4cf命令查看:

commit c4cf32be0098f786df455e3fee67ba21779dd70a
Author: yfm <imyangfm@gmail.com>
Date:   Mon Apr 29 16:30:31 2019 +0800

    first commit

可以看到我們在不使用高層命令的情況下,也完成了一個(gè)完整的提交歷史。

別高興的太早,我們我們使用git log命令查看,發(fā)現(xiàn)似乎少了點(diǎn)什么東西

→ git log
fatal: your current branch 'master' does not have any commits yet

使用git log命令查看,發(fā)現(xiàn)master分支還沒有任何提交信息,為什么呢?

對照test1項(xiàng)目,可以發(fā)現(xiàn)我們r(jià)efs/heads目錄下還少了個(gè)master文件。master文件執(zhí)行最近一次提交的引用。不可能每次通過哈希值去追溯提交歷史,我們可以起個(gè)簡單的名字方便我們記憶,Git默認(rèn)的分支名是master,我們就用這個(gè)名字代替提交對象的哈希值。

echo 'c4cf32be0098f786df455e3fee67ba21779dd70a' > .git/refs/heads/master

再次運(yùn)行g(shù)it log,發(fā)現(xiàn)已經(jīng)與test1項(xiàng)目完全一樣了,至此我們使用底層命令完成了git add和git commit所做的所有工作。

git log --pretty=oneline
c4cf32be0098f786df455e3fee67ba21779dd70a (HEAD -> master) first commit

總結(jié)下git commit時(shí)做了什么:

  1. 創(chuàng)建一個(gè)提交對象
  2. 創(chuàng)建一個(gè)指向改提交對象的master指針

最后用一張圖總結(jié):

版本庫

SHA-1檢驗(yàn)計(jì)算

哈希(hash)使用數(shù)據(jù)摘要算法(或稱散列算法),是信息安全領(lǐng)域中重要的理論基石。該算法將任意長度的輸入經(jīng)過散列運(yùn)算轉(zhuǎn)換為固定長度輸出。固定長度的輸出可以稱為對應(yīng)輸入內(nèi)容的數(shù)組摘要或哈希值。

前文看到的哪些哈希值是如何計(jì)算的呢?

下面內(nèi)容摘抄自:https://git-scm.com/book/zh/v2/Git-%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86-Git-%E5%AF%B9%E8%B1%A1

可以通過 irb 命令啟動(dòng) Ruby 的交互模式:

$ irb
>> content = "what is up, doc?"
=> "what is up, doc?"

Git 以對象類型作為開頭來構(gòu)造一個(gè)頭部信息,本例中是一個(gè)“blob”字符串。 接著 Git 會(huì)添加一個(gè)空格,隨后是數(shù)據(jù)內(nèi)容的長度,最后是一個(gè)空字節(jié)(null byte):

>> header = "blob #{content.length}\0"
=> "blob 16\u0000"

Git 會(huì)將上述頭部信息和原始數(shù)據(jù)拼接起來,并計(jì)算出這條新內(nèi)容的 SHA-1 校驗(yàn)和。 在 Ruby 中可以這樣計(jì)算 SHA-1 值——先通過 require 命令導(dǎo)入 SHA-1 digest 庫,然后對目標(biāo)字符串調(diào)用 Digest::SHA1.hexdigest():

>> store = header + content
=> "blob 16\u0000what is up, doc?"
>> require 'digest/sha1'
=> true
>> sha1 = Digest::SHA1.hexdigest(store)
=> "bd9dbf5aae1a3862dd1526723246b20206e5fc37"

Git 會(huì)通過 zlib 壓縮這條新內(nèi)容。在 Ruby 中可以借助 zlib 庫做到這一點(diǎn)。 先導(dǎo)入相應(yīng)的庫,然后對目標(biāo)內(nèi)容調(diào)用 Zlib::Deflate.deflate():

>> require 'zlib'
=> true
>> zlib_content = Zlib::Deflate.deflate(store)
=> "x\x9CK\xCA\xC9OR04c(\xCFH,Q\xC8,V(-\xD0QH\xC9O\xB6\a\x00_\x1C\a\x9D"

最后,需要將這條經(jīng)由 zlib 壓縮的內(nèi)容寫入磁盤上的某個(gè)對象。 要先確定待寫入對象的路徑(SHA-1 值的前兩個(gè)字符作為子目錄名稱,后 38 個(gè)字符則作為子目錄內(nèi)文件的名稱)。 如果該子目錄不存在,可以通過 Ruby 中的 FileUtils.mkdir_p() 函數(shù)來創(chuàng)建它。 接著,通過 File.open() 打開這個(gè)文件。最后,對上一步中得到的文件句柄調(diào)用 write() 函數(shù),以向目標(biāo)文件寫入之前那條 zlib 壓縮過的內(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

就是這樣——你已創(chuàng)建了一個(gè)有效的 Git 數(shù)據(jù)對象。 所有的 Git 對象均以這種方式存儲(chǔ),區(qū)別僅在于類型標(biāo)識(shí)——另兩種對象類型的頭部信息以字符串“commit”或“tree”開頭,而不是“blob”。 另外,雖然數(shù)據(jù)對象的內(nèi)容幾乎可以是任何東西,但提交對象和樹對象的內(nèi)容卻有各自固定的格式。

參考

https://git-scm.com/book/zh/v2

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容