1. 程式人生 > >Git工程開發實踐(二)——Git內部實現機制

Git工程開發實踐(二)——Git內部實現機制

trie 一段 時間戳 git分支 oss \n 保存 配置 -a

Git工程開發實踐(二)——Git內部實現機制

一、Git倉庫內部實現簡介

Git本質上是一個內容尋址(content-addressable)的文件系統,根據文件內容的SHA-1哈希值來定位文件。Git核心部分是一個簡單的鍵值對數據庫(key-value data store)。向Git數據庫插入任意類型的內容,會返回一個鍵值,通過返回的鍵值可以在任意時刻再次檢索(retrieve)插入的內容。通過底層命令hash-object可以將任意數據保存到.git目錄並返回相應的鍵值。
Git包含一套面向版本控制系統的工具集,包括高級命令和底層命令。高級命令主要由用戶使用,底層命令可以窺探Git內部的工作機制,但多數底層命令並不面向最終用戶,更適合作為新命令和自定義腳本的組成部分。

使用git init創建倉庫時,Git會創建一個.git目錄,其目錄結構如下:
技術分享圖片
A、description文件僅供GitWeb程序使用。
B、config文件包含項目特有的配置選項。
C、info目錄包含一個全局性排除(global exclude)文件,用於放置不希望被記錄在.gitignore文件中的忽略模式(ignored patterns)。
D、hooks目錄包含客戶端或服務端的鉤子腳本(hook scripts)。
E、objects目錄存儲所有數據內容,內有info、pack子目錄。
F、refs目錄存儲指向數據(分支)的提交對象的指針。
G、HEAD文件指示目前被檢出的分支。
H、index(尚待創建)文件保存暫存區信息。
objects、refs、HEAD、index是Git倉庫的四個核心部分。

二、Git對象

1、Git對象簡介

Git對象分為四種:數據對象(blob)、樹對象(tree)、提交對象(commit)、標簽對象(tag)。Git文件系統的設計思路與linux文件系統相似,即將文件的內容與文件的屬性分開存儲,文件內容存儲在文件系統中,文件名、所有者、權限等文件屬性信息則另外開辟區域進行存儲。
Git利用SHA-1加密算法對其管理的每一個文件生成一個唯一的16進制的40個字符長度的SHA-1哈希值來唯一標識對象。如果文件不變化,SHA-1哈希值不會改變;如果文件改變,會生成新的SHA-1哈希值。40位字符SHA-1哈希值的前兩個字符作為目錄名,後38個字符作為文件名,標識生成的Git對象。

Git對象的SHA-1哈希值計算公式如下:

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時需要註意:
A、header中第二部分關於數據長度的計算,一定是字節的長度而不是字符串的長度;
B、header + content的操作並不是字符串級別的拼接,而是二進制級別的拼接。
各種Git對象的hash方法相同,不同的在於:
A、頭部類型不同,數據對象是blob,樹對象是tree,提交對象是commit;
B、數據內容不同,數據對象的內容可以是任意內容,而樹對象和提交對象的內容有固定的格式。
git cat-file可以用來實現所有Git對象的讀取,包括數據對象、樹對象、提交對象的查看。
git cat-file -p [hash-key] 可以查看已經存在的object對象內容
git cat-file -t [hash-key] 可以查看已經存在的object對象類型

2、Git數據對象

2.1Git數據對象簡介

數據對象通常用於存儲文件的內容,但不包括文件名、權限等信息。數據對象和其對應文件的所在路徑、文件名是否改被更改都完全沒有關系。
Git會根據文件內容計算出一個SHA-1 hash值,以hash值作為索引將文件存儲在Git文件系統中。由於相同的文件內容的hash值是一樣的,因此Git將相同內容的文件只會存儲一次。git hash-object命令可以用來計算文件內容的hash值,並將生成的數據對象存儲到Git文件系統中。
echo -en "hello,git" | git hash-object --stdin
f28ffa36cdf69904e516babfdb3005e108dddfb7
在echo後面使用-n選項,用來阻止自動在字符串末尾添加換行符,否則會導致實際傳給git hash-object是hello,git\n
數據對象查看:
git show + 對象名(SHA1哈希值)

2.2Git數據對象的SHA-1哈希值計算

數據對象的內容格式如下:

blob <content length><NULL><content>

使用git hash-object計算文本的SHA1哈希值
echo -en "hello,git" | git hash-object --stdin
f28ffa36cdf69904e516babfdb3005e108dddfb7
使用openssl計算文本的SHA1哈希值:
echo -en "blob 9\0hello,git" | openssl sha1
(stdin)= f28ffa36cdf69904e516babfdb3005e108dddfb7
如果文本中有中文時,必須註意數據長度的計算是字節數而不是字符數。可以使用命令查看文本的字節數:
echo -n "中文" | wc -c

2.3Git數據對象的存取

git init Test //初始化一個版本庫
cd Test //進入Test
find .git/objects //查找.git/objects目錄下的內容

Git對objects目錄進行初始化,並創建pack和info目錄,但均為空。
echo ‘test content‘ | git hash-object -w --stdin //向Git數據庫存入文本
d670460b4b4aece5915caf5c68d12f560a9fe3e4 //返回的鍵值
-w選項指示hash-object命令存儲數據對象;若不指定此選項,則上述命令僅返回對應的鍵值。
--stdin選項則指示上述命令從標準輸入讀取內容,若不指定此選項,則須在命令尾部給出待存儲文件的路徑。
命令輸出一個長度為40個字符的校驗和,是一個SHA-1哈希值,一個將待存儲的數據外加一個頭部信息(header)一起做SHA-1校驗運算而得的校驗和。
find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
可以在objects目錄下看到一個文件。 Git存儲內容的方式是一個文件對應一條內容,用內容加上特定頭部信息一起的SHA-1校驗和為文件命名。校驗和的前兩個字符用於命名子目錄,余下的38個字符則用作文件名。
可以通過cat-file命令從Git數據庫取回數據。指定-p選項可指示cat-file命令自動判斷內容的類型,並顯示格式友好的內容:
git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

2.4Git版本控制原理的實現

通過對一個文件進行簡單的版本控制揭示Git版本控制的原理。首先,創建一個新文件並將其內容存入Git數據庫。

echo "version 1" > test //寫入test文件內容
git hash-object -w test  //存儲test文件到Git數據庫

83baae61804e65cc73a7201a7252750c76066a30

echo ‘version 2‘ > test  //寫入test文件新的內容
git hash-object -w test  //再次將修改後的test文件存儲到Git數據庫

1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
Git數據庫記錄了test文件的兩個不同版本。
find .git/objects -type f

.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a

恢復test文件到第一個版本:

git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test   
cat test   //讀取test文件內容

version 1
恢復test文件到第二個版本:

git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test
cat test  //讀取test文件內容

version 2
上述對文件的版本控制中,記住文件的每一個版本所對應的SHA-1值並不現實,並且文件名並沒有被保存。
利用cat-file -t命令,可以查看Git內部存儲的任何對象類型,只要給定對象的SHA-1值。
git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob
通過每一個數據對象的hash值,可以訪問Git文件系統中的任意數據對象,但記住數據對象的SHA-1哈希值顯然是不現實的。數據對象只是解決了文件內容存儲的問題,而文件名的存儲則需要通過樹對象實現。

3、Git樹對象

3.1Git對象簡介

樹對象包含指向數據對象或是其它樹對象的多個指針,用來表示內容之間的目錄層次關系。
Git所有內容均以樹對象和數據對象的形式存儲,其中樹對象對應UNIX中的目錄項,數據對象對應inodes或文件內容。 一個樹對象包含一條或多條樹對象記錄(tree entry),每條樹對象記錄含有一個指向數據對象或者子樹對象的SHA-1指針以及相應的模式、類型、文件名信息。
某項目當前對應的最新樹對象可以使用如下命令查看:
git cat-file -p master^{tree}
master^{tree}語法表示master分支上最新的提交所指向的樹對象。 目錄(所對應的樹對象記錄)並不是一個數據對象,而是一個指針,其指向的是另一個樹對象。
樹對象查看:
git show + 對象名/git ls-tree + 對象名
git ls-files --stage命令可以查看暫存區的內容。

3.2Git樹對象的SHA1哈希值計算

樹對象的內容格式如下:

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 -en "83baae61804e65cc73a7201a7252750c76066a30" | xxd -r -p > sha1.txt

構造content部分,並保存至文件content.txt

echo -en "100644 test.txt\0" | cat - sha1.txt > content.txt

計算content的長度
cat content.txt | wc -c
生成SHA-1
echo -en "tree 36\0" | cat - content.txt | openssl sha1
(stdin)= d8329fc1cc938780ffdd9f94e0d364e0ea74f579

3.3Git樹對象生成

Git根據某一時刻暫存區(即index文件)所表示的狀態創建並記錄一個對應的樹對象,如此重復便可依次記錄(某個時間段內)一系列的樹對象。 因此,為創建一個樹對象,首先需要通過暫存一些文件來創建一個暫存區。通過update-index為一個單獨文件(test.txt文件)的首個版本創建一個暫存區。 利用update-index命令,可以把test文件的首個版本加入一個新的暫存區。

git update-index --add --cacheinfo 100644   83baae61804e65cc73a7201a7252750c76066a30 test

--add表示新增文件名,如果第一次添加某一文件名,必須使用此選項;--cacheinfo mode object path是要添加的數據對象的模式、hash值和路徑,path意味著為數據對象不僅可以指定單純的文件名,也可以使用路徑。另外要註意的是,使用git update-index添加完文件後,一定要使用git write-tree寫入到Git文件系統中,否則只會存在於暫存區。
指定的文件模式為100644,表明是一個普通文件。 其它選擇包括:100755,表示一個可執行文件;120000表示一個符號鏈接。
現在可以通過write-tree命令將暫存區內容寫入一個樹對象。無需指定-w 選項,如果某個樹對象不存在,調用write-tree命令時會根據當前暫存區狀態自動創建一個新的樹對象。
git write-tree
5bf35b145b6281c080d58b6d19a5113a47f782ed
git cat-file -p 5bf35b145b6281c080d58b6d19a5113a47f782ed
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test
git cat-file -t 5bf35b145b6281c080d58b6d19a5113a47f782ed
tree
Git樹對象是在commit的過程中生成的,其生成會根據.git目錄下的index文件的內容來創建。git add的操作就是將文件的信息保存到index文件中,在commit時,根據index的內容來生成樹對象。
使用git update-index可以為數據對象指定名稱和模式,然後使用git write-tree將樹對象寫入到Git文件系統中。
創建一個新的樹對象,包括test.txt文件的第二個版本以及一個新的文件。

echo ‘new file‘ > new.txt
git update-index --cacheinfo 100644 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt

git update-index --add new.txt
暫存區現在包含test.txt文件的新版本和一個新文件new.txt,使用當前暫存區生成新的樹對象。
git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341

100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt

新的樹對象包含兩條文件記錄,同時test.txt的SHA-1值是第二版test.txt。 將第一個樹對象加入第二個樹對象,使其成為新的樹對象的一個子目錄。 通過調用read-tree命令可以把樹對象讀入暫存區。通過對 read-tree指定--prefix選項將一個已有的樹對象作為子樹讀入暫存區。

git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
git write-tree

3c4e9cd789d88d8d89c1073707c3585e41b0e614
git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614

040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt

如果基於新的樹對象創建一個工作目錄,工作目錄的根目錄包含兩個文件以及一個名為bak的子目錄,bak子目錄包含test.txt文件的第一個版本。
技術分享圖片
樹對象解決了文件名的問題,而且由於分階段提交樹對象,樹對象可以看做是開發階段源代碼目錄樹的一次次快照,因此可以用樹對象作為源代碼版本管理。但需要記住每個樹對象的hash值,才能找到各階段的源代碼文件目錄樹。在源代碼版本控制中,還需要知道誰提交了代碼、什麽時候提交的、提交的說明信息等,提交對象就是為了解決上述問題的。

4、Git提交對象

4.1Git提交對象簡介

提交對象指向一個樹對象,並且帶有相關的描述信息,標記項目某一個特定時間點的狀態。提交對象包含一些關於時間點的元數據,如時間戳、最近一次提交的作者、指向上次提交的指針等等。
提交對象查看如下:
git show / git log + -s + --pretty=raw +對象名

4.2Git提交對象的生成

提交對象是用來保存提交的作者、時間、說明這些信息的,可以使用git commit-tree來將提交對象寫入到Git文件系統中。
echo ‘first commit‘ | git commit-tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
162f9174ac6bb4c5d41bfc00fcb5147e2d62b839
commit-tree除了要指定提交的樹對象,也要提供提交說明,但提交的作者和時間則根據環境變量自動生成,並不需要指定。由於提交的作者和時間不同,提交對象的SHA-1哈希值也不相同。
提交對象的查看可以使用git cat-file。
git cat-file -p 162f9174ac6bb4c5d41bfc00fcb5147e2d62b839

tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author scorpio <[email protected]> 1536497938 +0800
committer scorpio <[email protected]> 1536497938 +0800

first commit

非首次提交需要指定使用-p指定父提交對象,使代碼版本才能成為一條時間線。
echo ‘second commit‘ | git commit-tree 0155eb -p 162f9174ac6bb4c5d41bfc00fcb5147e2d62b839
f6bbc9d4e8de1b35ad66c2115aa8519587c26100
git cat-file查看一下新的提交對象,看到相比於第一次提交,多了parent部分。
第三次提交:
echo ‘third commit‘ | git commit-tree 3c4e9c -p f6bbc9d4e8de1b35ad66c2115aa8519587c26100
第三次提交查看:
git log --stat 26a72965aa9c1bdab9fe5972012bd903f501f006 --pretty=oneline

26a72965aa9c1bdab9fe5972012bd903f501f006 third commit
 bak/test.txt | 1 +
 1 file changed, 1 insertion(+)
f6bbc9d4e8de1b35ad66c2115aa8519587c26100 second commit
 new.txt  | 1 +
 test.txt | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)
162f9174ac6bb4c5d41bfc00fcb5147e2d62b839 first commit
 test.txt | 1 +
 1 file changed, 1 insertion(+)

最終提交對象的結構圖:
技術分享圖片
合並的提交(merge commits)可能會有不只一個父對象。如果一個提交對象沒有父對象,稱為根提交(root commit),代表項目最初的一個版本(revision)。每個項目必須有至少有一個根提交(root commit)。

4.3Git提交對象的SHA-1哈希值計算

提交對象的內容格式如下:

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>

第一次提交的提交對象的內容如下:
git cat-file -p 162f9174ac6bb4c5d41bfc00fcb5147e2d62b839

tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author scorpio <[email protected]> 1536497938 +0800
committer scorpio <[email protected]> 1536497938 +0800

first commit

使用openssl計算SHA-1

echo -n "commit 165\0
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author scorpio <[email protected]> 1536497938 +0800
committer scorpio <[email protected]> 1536497938 +0800

first commit" | openssl sha1

5、Git對象存儲

Git中的數據對象解決了數據存儲的問題,樹對象解決了文件名存儲問題,提交對象解決了提交信息的存儲問題。
Git對象(數據對象、樹對象和提交對象)都存儲在.git/objects目錄下。
技術分享圖片
Git對象的40位SHA-1哈希值分為兩部分:前兩位作為目錄名稱,後38位作為對象文件名。
Git對象的存儲路徑規則為:.git/objects/hash[0, 1]/hash[2, 40]
Git對象存儲的算法步驟:
A、計算content長度,構造header;
B、將header添加到content前面,構造Git對象;
C、使用sha1算法計算Git對象的40位hash碼;
D、使用zlib的deflate算法壓縮Git對象;
E、將壓縮後的Git對象存儲到.git/objects/hash[0, 2]/hash[2, 40]路徑下;
使用Nodejs來實現git hash-object -w的功能,即計算Git對象的hash值並存儲到Git文件系統中:

const fs = require(‘fs‘)
const crypto = require(‘crypto‘)
const zlib = require(‘zlib‘)
function gitHashObject(content, type) {
??// 構造header
??const header = `${type} ${Buffer.from(content).length}\0`
??// 構造Git對象
??const store = Buffer.concat([Buffer.from(header), Buffer.from(content)])
??// 計算hash
??const sha1 = crypto.createHash(‘sha1‘)
??sha1.update(store)
??const hash = sha1.digest(‘hex‘)
??// 壓縮Git對象
??const zlib_store = zlib.deflateSync(store)
??// 存儲Git對象
??fs.mkdirSync(`.git/objects/${hash.substring(0, 2)}`)
??fs.writeFileSync(`.git/objects/${hash.substring(0, 2)}/${hash.substring(2, 40)}`, zlib_store)
??console.log(hash)
}
// 調用入口
gitHashObject(process.argv[2], process.argv[3])

測試:
node index.js ‘hello, world‘ blob
8c01d89ae06311834ee4b1fab2f0414d35f01102
git cat-file -p 8c01d89ae06311834ee4b1fab2f0414d35f01102
hello, world

三、Git引用

1、Git引用簡介

Git操作中經常需要瀏覽完整的提交歷史,但為了能遍歷提交歷史從而找到所有相關對象,必須記住最後一個提交對象的SHA1哈希值。因此,需要一個文件來保存SHA-1值,並給文件起一個簡單名字,然後用名字來替代原始的 SHA-1值。可以在.git/refs目錄下找到含有SHA-1值的文件。
find .git/refs

.git/refs
.git/refs/heads
.git/refs/tags

如果需要創建一個新引用來幫助記錄最新提交所在的位置,從技術上只需將最新提交對象的SHA1哈希值寫入引用文件內:

 echo "524fd8729bbee740392739d22f64784ec81a9804" > .git/refs/heads/test

然後就可以在Git命令中使用剛創建的新引用來代替SHA-1值。
git log --pretty=oneline test
通常,不提倡直接編輯引用文件。 如果想更新某個引用,Git提供了一個更加安全的命令update-ref來編輯引用。
Git分支的本質上是一個指向某一系列提交之首的指針或引用。 若想在某個提交對象上創建一個分支,可以進行如下操作:
git update-ref refs/heads/newbranchname commit_id

2、HEAD引用

當執行git branch (branchname)時,Git通過HEAD文件獲取最新提交對象的SHA-值。HEAD文件是一個符號引用(symbolic reference),不像普通引用包含一個SHA-1值,而是一個指向其它引用的指針,指向當前所在的分支。 可以查看HEAD文件的內容:
cat .git/HEAD
ref: refs/heads/master
如果執行git checkout test,Git會對HEAD文件進行更新。
cat .git/HEAD
ref: refs/heads/test
當執行git commit時,會創建一個提交對象,並用HEAD文件中引用所指向的SHA-1值設置其父提交字段。

3、標簽引用