在上一篇 文章 中,将了資料對象、樹對象和送出對象三種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