理解了這個 3 個 object,你甚至能自己寫個 git!

git 我們每天都在用,但你知道它是怎麼實現的麼?

git add、git commit 整天都敲,但你知道它底層做了什麼麼?

commit、branch、暫存區這些都是怎麼實現的,怎麼做到的版本切換呢?

所有這些疑問,只要搞懂 3 個 object 就全部能解答了。

不信我們來看一下:

首先,執行 git init 初始化 git 倉庫。

git 的所有內容都是存儲在 .git 這個隱藏目錄的,我們先把它給搞出來:

默認隱藏,但只要你把這個 exclude 配置刪掉,就顯示出來了:

展開以後可以看到這些東西:

重點就是這裏的 objects。

它是什麼呢?

我們添加一個 object 就知道了:

有這樣一個 text.txt 的文件:

執行這個 hash-object 的命令:

git hash-object -w text.txt

它會返回一個 hash:

然後你會在 objects 目錄下發現多了一個目錄,目錄名是 hash 前兩位,剩下的是文件名:

它存了什麼內容呢?

可以通過 cat-file 來看:

git cat-file -p 7c4a013e52c76442ab80ee5572399a30373600a2

-p 是 print 的意思。

可以看到文件內容就是 text.txt 的內容:

哦,原來 git 存儲的文件內容就是放在這裏的。

改一下文件內容,再存一下:

git hash-object -w text.txt

你會看到多了一個新的目錄,同樣是 hash 做目錄名和文件名:

就這麼一點東西,我們就能實現版本管理了!

怎麼做呢?

讀取不同 hash 的內容寫入文件不就行了?

比如現在內容是 bbb,我想恢復上一個版本的內容是不是隻要 cat-file 上個 hash 再寫入文件就行了?

git cat-file -p 7c4a013e52c76442ab80ee5572399a303 > text.txt

這就是一個版本管理工具了!

當然,現在還沒有存文件名的信息,還有目錄信息,這些信息存在哪呢?

這就需要別的類型的 object 了。

剛纔我們看的存儲文件內容的 object 叫做 blob。

可以通過 cat-file 加個 -t 看出來:

-t 是 type 的意思。

git cat-file -t 7c4a013e52c76442ab80ee5572399a303

還有存儲目錄和文件名的 object,叫做 tree。

tree 和 blob 是咋關聯的呢?

找個真實的倉庫看看就知道了:

比如我在 react 項目下執行了 cat-file,之前我們用它查看過 blob 對象內容,這次查看的是 main 分支的頂部的 tree 對象。

git cat-file -p main^{tree}

可以看到有很多 blob 對象和 tree 對象:

很容易看出來,目錄是 tree 對象,文件內容是 blob 對象:

那文件名呢?

文件名不是已經在 tree 對象裏包含了麼?

我們繼續用 cat-file 看下 packages 這個 tree 對象的內容:

git cat-file -p 2889ab8f0ef04484849c40d3eebe330ec25bbe1c

很容易就可以看出來 git 是怎麼存儲一個目錄的了:

在 tree 對象裏存儲每個子目錄和文件的名字和 hash。

在 blob 對象裏存儲文件內容。

tree 對象裏通過 hash 指向了對應的 blob 對象。

這樣是不是就串起來了!

這就是 git 存儲文件的方式。

那這個 hash 是怎麼算出來的呢?

也很簡單,是對 “對象類型 內容長度 \ 0 內容” 的字符串 sha1 之後的值轉爲 16 進制字符串。

比如 aaa 的 hash 就是這樣算的:

const crypto = require('crypto');

function hash(content) {
    const sha1 = crypto.createHash('sha1');
    sha1.update(content);
    return sha1.digest('hex');
}

console.log(hash('blob 3\0aaa'))

是不是一毛一樣!

所有的 object 都是這麼算 hash 的。

繼續來講 tree 對象:

其實我們放到暫存區的內容就相當於一個新的目錄,也是通過 tree 對象存儲的。

更新暫存區用 update-index 這個命令:

git update-index --add --cacheinfo 100644 7c4a013e52c76442ab80ee5572399a30373600a2 text.txt

--add --cacheinfo 就是往暫存區添加內容。

指定文件名和 hash,這裏我們把 aaa 那個文件放進去了。

前面的 100644 是文件模式:

100644 是普通文件,100755 是可執行文件,120000 是符號鏈接文件。

添加之後就可以看到 .git/index 這個文件了,暫存區的內容就是放在這:

這時候你執行 git status 就可以看到暫存區已經有這個文件了:

所以說,git add 的底層就是執行了 git update-index。

然後暫存區的內容寫入版本庫的話只要執行下 write-tree 就好了:

git write-tree

然後你就會發現它返回了一個 hash,並且 objects 目錄下多了一個 object:

這個對象是啥類型呢?

通過 cat-file -t 看下就知道了:

git cat-file -t 9ef7e5a61a3b70ff7149805fc86a4c26e953bb3f

可以看到,是個 tree 對象:

所以說,暫存區的內容是作爲 tree 對象保存的。

再 cat-file -p 看下它的內容:

git cat-file -t 9ef7e5a61a3b70ff7149805fc86a4c26e953bb3f

可以看到是這樣的:

這就是 git commit 的原理了。

現在假設有個需求,讓你找到某個版本的某個文件的內容,恢復回去。

是不是就很簡單了?

只要找到對應版本的那個 tree 的 hash,然後再一層層找到對應的 blob 對象,讀取內容再寫入文件就好了!

這就是 git revert 的原理了。

當然,要是每個版本都要自己記住頂層 tree 的 hash 也太麻煩了。

所以 git 又設計了 commit 對象。

可以通過 commit-tree 命令把某個 tree 對象創建一個 commit 對象。

echo 'guang 111' | git commit-tree 9ef7e5

這裏的參數就是上面的 tree 對象的 hash:

再用 cat-file -t 看看返回的對象的類型:

git cat-file -t b5f92e68912595dbb3b6cbda9123838546b18f7d

確實,這是一個 commit 對象:

那 commit 對象都存了啥呢?

還是用 cat-file -p 看看:

git cat-file -p b5f92e68912595dbb3b6cbda9123838546b18f7d

下面的內容很熟悉,但是多了一個 tree 節點的指向,這個很正常,commit 的內容就是某個 tree 所對應的版本嘛。

commit、tree、blob 三個對象就是這樣的關係:

commit 之間還能關聯,也就是有先後順序。

這個用 commit-tree -p 來指定:

比如我們再創建兩個 commit:

echo 'guang 111' | git commit-tree 9ef7e5 -p b5f92e6
echo 'guang 222' | git commit-tree 9ef7e5 -p c3f9f5

這時你用 git log 看看:

git log 1d1234

你會看到平時經常看到的 commit 歷史:

這就是 commit 的實現原理!

當然,這裏要記 commit 的 hash 同樣也很麻煩。

平時我們怎麼用呢?

用 branch 或者 tag 呀!

branch 和 tag 其實就是記錄了這個 commit 的 hash。

這部分就不是 object 了,叫做 ref:

創建 ref 使用 update-ref 的命令:

git update-ref refs/heads/guang 1d1234e77de6de0bb8edcf90cbd1a9546d7b1d9a

比如我創建了一個叫做 guang 的指向一個 commmit 對象的 ref。

這裏就會多一個文件,內容存着指向的 commit 是啥:

然後你 git branch 看看:

其實這就是創建了一個新的分支。

這就是 branch 的原理。

tag 也是一樣,只不過它是放在 refs/tags 目錄下的:

git update-ref refs/tags/v1.0 1d1234e77de6de0bb8edcf90cbd1a9546d7b1d9a

blob、tree、commit  和 ref 的關係就是這樣的:

總結

今天我們探究了 git 的實現原理,主要是 3 個 object 以及兩個 ref。

3 個對象是:

2 個 ref 是:

branch:指向某個 commit      tag:指向某個 commit

此外,暫存區放在 .git/index 文件裏,內容其實也是個 tree 對象的內容。

還有,hash 的計算方式是類似 blob 3\0aaa 這樣 “對象類型 內容長度 \ 0 內容” 的格式,對它做 sha1 然後轉爲十六進制。

基本看懂這張圖就好了:

理解了這些,你就能理解 git add、git commit、git log、git revert、git branch、git tag 等等絕大多數 git 命令的實現原理了。

甚至按照這個思路來,自己寫一個 git 是不是也不難呢?

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/FNrFXgp1uqobMcuPjo5mvA