1. 程式人生 > >Git 內部原理之 Git 物件雜湊

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部分組成:第一部分表示物件的型別,可以取值blobtreecommit

以分別表示資料物件、樹物件、提交物件;第二部分是資料的位元組長度;第三部分是一個空位元組,用來將headercontent分隔開。將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-objectopenssl 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

我們首先使用xxd83baae61804e65cc73a7201a7252750c76066a30轉換成為二進位制形式,並將結果儲存為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