天天看點

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

繼續閱讀