從.git文件夾探析git實現原理


git是一款分布式代碼版本管理工具,通過git能夠更加高效地協同編程。了解git的工作原理將有助於我們使用git工具更好地管理項目。通過了解.git文件夾中的文件組成,我們可以從一個角度去窺探git的實現原理。我們知道,在開始開發一個項目或加入一個項目時,需要創建一個新的倉庫git init [options],或從遠端克隆一個已經存在的倉庫git clone [uri],除使用git init --bare創建一個“裸”倉庫以外,所有創建的本地倉庫都包含有一個.git文件夾,需要了解的是,“裸”倉庫的內容就是.git文件夾中的內容。討論“裸”倉庫與實際倉庫作用的異同不是我們現在討論的重點,如需了解可翻閱相關文檔。由此,git系統當中的的所有數據都存在於.git文件夾之中。

.git文件夾中的內容及作用

打開.git文件夾后,通常有五個文件夾:

  • hooks文件夾,用於存儲shell腳本,當執行某些git指令后,會觸發存儲在該文件夾下指定的shell腳本
  • info文件夾,用於存儲該項目倉庫的相關信息
  • logs文件夾,用於記錄分支提交記錄
  • objects文件夾,“key-value數據庫”
  • refs文件夾,用於記錄每個分支的最新提交結點以及tags

我們需要着重關注logs文件夾、objests文件夾以及refs文件夾,通過這三個文件夾所存儲的內容來分析git。在.git文件夾中,同樣存在有一些文件,譬如HEAD、config、index等文件,其中HEAD文件用以記錄當前倉庫指向的項目提交結點,config文件中記錄着倉庫的配置信息,這些文件內容不是我們要討論的重點。

objects文件夾,git中的“key-value數據庫”

在討論objects文件夾的內容之前,我們需要明確存在於git系統中的三個實體,即“提交結點”“節點內容”“文件內容”
objects可以認為是一種“key-value數據庫”,之所以將數據庫打引號,是因為這個“git的數據庫”不具備數據庫的基本功能,而僅僅具備可以通過key值能夠找到與之對應的value。

提交結點實體,是整個git中的核心實體,提交節點中描述了提交節點之間的繼承關系,即本次提交的內容是基於哪個或哪幾個之前的提交的內容,提交結點實體之間的關系形成了一個DAG圖,通過這個DAG圖可以清晰地理順整個項目的發展脈絡,提交節點的內容如下:

tree <SHA1-signature>
[
    parent <SHA1-signature>
    ...
]
author <author name> <\<author email\>> <timestamp> <time zone>
committer <committer name> <\<committer email\>> <timestamp> <time zone>

<commit message>

tree用於指向與該提交結點實體關聯的節點內容實體。parent用於指向該提交節點實體所基於的之前的提交結點實體,可以看到,parent可以是多個。author用於記錄本次提交的作者姓名、作者郵箱、作者所添加的內容時間以及時區。committer用於記錄本次提交的提交者姓名、郵箱等內容。commit message用於記錄當前提交的消息日志。

節點內容實體,用於記錄本次提交時,提交中所包含的所有文件名,以及文件名所對應的key值,值得注意的是,可能由於查詢性能的緣故,並非是僅記錄本次提交時修改的文件,而是記錄本次提交時所有的文件。另有一點值得注意的是,即便項目倉庫中的文件不變,僅改變某個或某幾個文件內容的前后兩次提交,生成的前后兩次提交節點中的tree值是不同的,換句話說,節點內容與提交節點是邏輯上的一對一關系。隨着之后的討論我們會很自然地得出這樣的結論,這種一對一關系也同樣是必須的,盡管在實際情況中允許兩個不同的提交節點實體指向相同的節點內容實體。

文件內容實體,用於記錄具體的文件內容。也就是說,在一個git倉庫中,並非只有程序員們所能看到的當前項目文件夾下的代碼版本,包括所有的歷史代碼都會在.git文件夾中有一個備份。

在objects文件夾中,三種數據實體無差別的以key-value的形式進行存儲。因此一次提交操作,在objects文件夾中至少生成兩個文件。存儲時采用deflate算法對原始文件內容進行壓縮,而key值是根據原始文件內容、文件大小等數據生成的消息摘要,在當前版本的git中,消息摘要生成算法采用SHA1算法,生成過程是將文件格式與文件長度組成頭部,將文件內容作為尾部,由頭部和尾部拼接后作為原文,經過SHA1算法計算之后得到該文件的160位長的SHA1簽名。為防止一個文件夾內的文件數量過多,將簽名每四位用字符表示十六進制數,於是得到一個長度為40的字符串,將字符串的前兩個字符作為文件夾,后38個字符作為文件名進行存儲。

觀察仔細的同學可以發現,在三個實體的內容里,沒有任何一個字段提供分支概念的信息。

logs文件夾,用於記錄分支提交記錄

該文件夾下的內容是一條分支下的所有提交節點實體序列。在該文件夾下,文件內容格式是單一的,即形如這樣:

0000000000000000000000000000000000000000 6a0fa53d78f03abea3439b9213123d1f260f5beb author <mail> 1511776312 +0800	commit (initial): master 1
6a0fa53d78f03abea3439b9213123d1f260f5beb 75642040a2da5b324befde7ca8531b3426b32ba7 author <mail> 1511776323 +0800	commit: master 2

...

在一個分支創建時,無論這個分支是master還是基於某個提交結點創建的子分支,在logs文件中關於分支的時間線總是以全0的值為開始的。我們需要關注這樣幾個問題:

  1. master在初始化時是否會創建一個起始地提交結點?
  2. 分支創建時的是否會創建一個新的提交節點?

通過 git init創建一個初始化的git倉庫時,master是默認創建的,在初始化的git倉庫中,.git文件夾中是不存在logs文件夾的,且在objects文件夾中不包含任何key-value鍵值對,甚至不存在一個實際存在的master主分支,因此所謂的master初始化並非是在git倉庫初始化時進行的,而是在首次提交時進行的。在測試項目中,我在以75為開頭的提交結點時創建了一個子分支,查看子分支的logs文件內容:

0000000000000000000000000000000000000000 75642040a2da5b324befde7ca8531b3426b32ba7 author <mail> 1511776334 +0800	branch: Created from HEAD

...

可以看到,子分支創建時並非將主分支上分叉節點復制一下,而是從這個節點起即為一個子分支。

當合並分支時,logs的日志是如何表現,事先需要明確的是,合並分支等價於一次提交(合並分支會生成一個提交結點實體)。我們需要關注這樣幾個問題:

  1. 在子分支上已經提交過若干次,在父分支上不提交代碼,當在父分支上合並子分支時,父分支的logs記錄序列是怎樣的
  2. 在子分支上提交若干次,在父分支上同樣提交若干次,當在父分支上合並子分支時,父分支的logs記錄序列是怎樣的

關於這兩個問題,我們需要觀察相關的文件。第一個實驗是,首先創建了一個倉庫,並在master分支上提交了一次代碼,之后在master分支的最新提交結點上創建了一個子分支branch,再在branch分支上連續提交了兩次代碼,而后切換到mster分支后,合並branch分支,logs文檔的記錄如下:

該文件是master分支的logs文件:

0000000000000000000000000000000000000000 79f586c23a8a169f1651411c879657406757ef92 author <mail> 1511853763 +0800	commit (initial): master 1
79f586c23a8a169f1651411c879657406757ef92 ea4fbfc8600b90555a8a4eb410a176cfbdfa48d7 author <mail> 1511853908 +0800	merge branch: Fast-forward

該文件是branch分支的logs文件

0000000000000000000000000000000000000000 79f586c23a8a169f1651411c879657406757ef92 author <mail> 1511853776 +0800	branch: Created from master
79f586c23a8a169f1651411c879657406757ef92 2dbe03d87733bbcf5b760ba3beacd61ab3f54b58 author <mail> 1511853808 +0800	commit: branch 1
2dbe03d87733bbcf5b760ba3beacd61ab3f54b58 ea4fbfc8600b90555a8a4eb410a176cfbdfa48d7 author <mail> 1511853870 +0800	commit: branch 2

可以看到,在父分支沒有做改動且子分支做改動的情況下,由父分支進行合並時,是直接將父分支的最新分支節點定義為子分支上的最新分支節點,ea開頭的提交節點實體的內容如下:

tree 1247c7d74e9c28fb83e8e394910346dee104fcae
parent 2dbe03d87733bbcf5b760ba3beacd61ab3f54b58
author author <mail> 1511853870 +0800
committer author <mail> 1511853870 +0800

...

第二個實驗是在父分支上提交若干次切在子分支上提交若干次,在父分支上合並子分支時,logs的數據內容。首先創建一個倉庫,且在master分支上提交一次代碼。在該提交的代碼基礎上創建一個分支branch。分別在branch分支和在master分支上各自提交兩次代碼(無沖突),再在master分支上合並branch分支,觀察兩個分支下的文件內容:

該文件是master分支的logs文件:

0000000000000000000000000000000000000000 8bf95277d6c020f0ade434896355c448fd0cac00 author <mail> 1511854786 +0800	commit (initial): 1
8bf95277d6c020f0ade434896355c448fd0cac00 368ddb7d615e5eedfe5813ff2f88547dcb66b02b author <mail> 1511854819 +0800	commit: change
368ddb7d615e5eedfe5813ff2f88547dcb66b02b 3ba031189d37d816421a019a6240e9d79a683fdd author <mail> 1511854837 +0800	commit: master add 2
3ba031189d37d816421a019a6240e9d79a683fdd 12c6263013cf467f348549337813db01044046b9 author <mail> 1511854896 +0800	merge branch: Merge made by the 'recursive' strategy.

該文件是branch分支的logs文件

0000000000000000000000000000000000000000 8bf95277d6c020f0ade434896355c448fd0cac00 author <mail> 1511854795 +0800	branch: Created from master
8bf95277d6c020f0ade434896355c448fd0cac00 ba65e4cc73953f522f14a47088acf814e26ebc29 author <mail> 1511854859 +0800	commit: add 3
ba65e4cc73953f522f14a47088acf814e26ebc29 eccc7d8176edb5064cde7649ad651bb2e4fce0e3 author <mail> 1511854873 +0800	commit: add 4

可以看出,在執行merge之后,master分支的最后一次提交結點實體是兩個logs文件中都從未出現過的,並且,這個以12為開頭的最新提交結點實體的前一個結點實體,是父節點的次新提交結點實體。

額外關注一下以12為開頭的最新提交結點實體的內容:

tree 659231fc28ddcd98c8d557e07a3fb5de4efc55b6
parent 3ba031189d37d816421a019a6240e9d79a683fdd
parent eccc7d8176edb5064cde7649ad651bb2e4fce0e3
author author <mail> 1511854896 +0800
committer author <mail> 1511854896 +0800


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM