理解了這個 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 個對象是:
-
blob:存儲文件內容
-
tree:存儲目錄結構和文件名,指向 blob 和 tree
-
commit:存儲版本信息,指向不同版本的入口 tree
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