Git 內部原理之 Git 物件雜湊
在上一篇文章中,將了資料物件、樹物件和提交物件三種Git物件,每種物件會計算出一個hash值。那麼,Git是如何計算出Git物件的hash值?本文的內容就是來解答這個問題。
Git物件的hash方法
Git中的資料物件、樹物件和提交物件的hash方法原理是一樣的,可以描述為:
header = "<type> " + content.length + "\0"
hash = sha1(header + content)
上面公式表示,Git在計算物件hash時,首先會在物件頭部新增一個header
。這個header
由3部分組成:第一部分表示物件的型別,可以取值blob
、tree
、commit
header
和content
分隔開。將header
新增到content
頭部之後,使用sha1
演算法計算出一個40位的hash值。
在手動計算Git物件的hash時,有兩點需要注意:
1.header
中第二部分關於資料長度的計算,一定是位元組的長度而不是字串的長度;
2.header + content
的操作並不是字串級別的拼接,而是二進位制級別的拼接。
各種Git物件的hash方法相同,不同的在於:
1.頭部型別不同,資料物件是blob
,樹物件是tree
,提交物件是commit
;
2.資料內容不同,資料物件的內容可以是任意內容,而樹物件和提交物件的內容有固定的格式。
接下來分別講資料物件、樹物件和提交物件的具體的hash方法。
資料物件
資料物件的格式如下:
blob <content length><NULL><content>
從上一篇文章中我們知道,使用git hash-object
可以計算出一個40位的hash值,例如:
$ echo -n "what is up, doc?" | git hash-object --stdin
bd9dbf5aae1a3862dd1526723246b20206e5fc37
注意,上面在echo
後面使用了-n
選項,用來阻止自動在字串末尾新增換行符,否則會導致實際傳給git hash-object
what is up, doc?\n
,而不是我們直觀認為的what is up, doc?
。
為驗證前面提到的Git物件hash方法,我們使用openssl sha1
來手動計算what is up, doc?
的hash值:
$ echo -n "blob 16\0what is up, doc?" | openssl sha1
bd9dbf5aae1a3862dd1526723246b20206e5fc37
可以發現,手動計算出的hash值與git hash-object
計算出來的一模一樣。
在Git物件hash方法的注意事項中,提到header
中第二部分關於資料長度的計算,一定是位元組的長度而不是字串的長度。由於what is up, doc?
只有英文字元,在UTF8中恰好字元的長度和位元組的長度都等於16,很容易將這個長度誤解為字元的長度。假設我們以中文
來試驗:
$ echo -n "中文" | git hash-object --stdin
efbb13322ba66f682e179ebff5eeb1bd6ef83972
$ echo -n "blob 2\0中文" | openssl sha1
d1dc2c3eed26b05289bddb857713b60b8c23ed29
我們可以看到,git hash-object
和openssl sha1
計算出來的hash值根本不一樣。這是因為中文
兩個字元作為UTF格式儲存後的字元長度不是2,具體是多少呢?可以使用wc
來計算:
$ echo -n "中文" | wc -c
6
中文
字串的位元組長度是6,重新手動計算髮現得出的hash值就能對應上了:
$ echo -n "blob 6\0中文" | openssl sha1
efbb13322ba66f682e179ebff5eeb1bd6ef83972
樹物件
樹物件的內容格式如下:
tree <content length><NUL><file mode> <filename><NUL><item sha>...
需要注意的是,<item sha>
部分是二進位制形式的sha1碼,而不是十六進位制形式的sha1碼。
我們從上一篇文章摘出一個樹物件做實驗,其內容如下:
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt
我們首先使用xxd
把83baae61804e65cc73a7201a7252750c76066a30
轉換成為二進位制形式,並將結果儲存為sha1.txt
以方便後面做追加操作:
$ echo -n "83baae61804e65cc73a7201a7252750c76066a30" | xxd -r -p > sha1.txt
$ cat tree-items.txt
���a�Ne�s� rRu
vj0%
接下來構造content部分,並儲存至檔案content.txt
:
$ echo -n "100644 test.txt\0" | cat - sha1.txt > content.txt
$ cat content.txt
100644 test.txt���a�Ne�s� rRu
vj0%
計算content的長度:
$ cat content.txt | wc -c
36
那麼最終該樹物件的內容為:
$ echo -n "tree 36\0" | cat - content.txt
tree 36100644 test.txt���a�Ne�s� rRu
vj0%
最後使用openssl sha1
計算hash值,可以發現和實驗的hash值是一樣的:
$ echo -n "tree 36\0" | cat - content.txt | openssl sha1
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
提交物件
提交物件的格式如下:
commit <content length><NUL>tree <tree sha>
parent <parent sha>
[parent <parent sha> if several parents from merges]
author <author name> <author e-mail> <timestamp> <timezone>
committer <author name> <author e-mail> <timestamp> <timezone>
<commit message>
我們從上一篇文章摘出一個提交物件做實驗,其內容如下:
$ echo 'first commit' | git commit-tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
db1d6f137952f2b24e3c85724ebd7528587a067a
$ git cat-file -p db1d6f137952f2b24e3c85724ebd7528587a067a
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author jingsam <[email protected]> 1528022503 +0800
committer jingsam <[email protected]> 1528022503 +0800
first commit
這裡需要注意的是,由於echo 'first commit'
沒有新增-n
選項,因此實際的提交資訊是first commit\n
。使用wc
計算出提交內容的位元組數:
$ echo -n "tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author jingsam <[email protected]> 1528022503 +0800
committer jingsam <[email protected]> 1528022503 +0800
first commit\n" | wc -c
163
那麼,這個提交物件的header
就是commit 163\0
,手動把頭部新增到提交內容中:
commit 163\0tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author jingsam <[email protected]> 1528022503 +0800
committer jingsam <[email protected]> 1528022503 +0800
first commit\n
使用openssl sha1
計算這個上面內容的hash值:
$ echo -n "commit 163\0tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author jingsam <[email protected]> 1528022503 +0800
committer jingsam <[email protected]> 1528022503 +0800
first commit\n" | openssl sha1
db1d6f137952f2b24e3c85724ebd7528587a067a
可以看見,與實驗的hash值是一樣的。
總結
這篇文章詳細地分析了Git中的資料物件、樹物件和提交物件的hash方法,可以發現原理是非常簡單的。資料物件和提交物件打印出來的內容與儲存內容組織是一模一樣的,可以很直觀的理解。對於樹物件,其打印出來的內容和實際儲存是有區別的,增加了一些實現上的難度。例如,使用二進位制形式的hash值而不是直觀的十六進位制形式,我現在還沒有從已有資料中搜到這麼設計的理由,這個問題留待以後解決。
原文地址:https://jingsam.github.io/2018/06/10/git-hash.html