一文讀懂git核心工作原理

lusq發表於2020-01-27

寫在前面

你是否經常學習git,然後發現還是無法完全掌握它的使用?曾經我也感覺學習了很多git的使用技巧,查閱了很多文件和教程,但當碰到很多場景和問題時還是一臉茫然。直到我深入學習了git的工作原理,再去結合實踐中的場景和問題後,才慢慢有了一種融會貫通的感覺。當理解了問題的原理後,不管是使用還是解決實踐中的問題,都會變得遊刃有餘

git原理剖析

commit到底是什麼

先來看一張圖

一文讀懂git核心工作原理

其實git內部一切皆物件,commit物件是git倉庫的一個快照,包含了整個專案在某一次提交時所有的檔案。從圖中可以看到一個commit物件在git內部實際上指向一個tree物件,這個tree是專案的根路徑,然後遞迴指向下面的tree和blob物件,blob則是單個檔案的二進位制儲存。有人可能會有疑惑,如果每個commit都包含整個倉庫,那如果兩個commit之間只修改了一個檔案,那其他所有檔案不是都冗餘儲存了嗎?實際上當然不會,再來看一張圖

一文讀懂git核心工作原理

圖中每個version其實就是一個commit,而虛線框的檔案表示不同commit中該檔案未做修改,git內部實際上對同一個檔案(blob物件)的儲存永遠只有一份,不同commit中是用物件引用的形式來指向同一檔案

探祕.git檔案

你有沒有想過,為什麼當你從git倉庫中clone一個專案後,就可以進行各種git操作來進行團隊協作了?git其實也是計算機上的一個程式,加上它所需要的資料就能開始工作了,而這些資料全部都存放在專案的.git檔案中。下圖是一個普通git專案的.git資料夾

一文讀懂git核心工作原理
我們先重點關注下這個objects資料夾,cd進去

一文讀懂git核心工作原理
可以看到一堆名字是兩個字元的資料夾,隨便找一個cd進去(我找了00這個資料夾)

一文讀懂git核心工作原理
這次是一些一長串字元作為檔名的檔案(實際上剛才的上層目錄名00加上當前目錄的一個檔名就得到了git中的一個物件,git用一個40個字元的hash來標識一個物件,它可能是commit、tree或者是blob),我們用cat命令檢視一下檔案的內容

一文讀懂git核心工作原理
可以看到結果是亂碼,實際上它是一個blob物件,也就是二進位制儲存的檔案,這裡我們可以用另外一個git命令,cat-file來檢視一下它轉化後的內容

一文讀懂git核心工作原理
這個實際上就是倉庫裡某一個版本中,也就是某一次commit中的一個檔案的具體內容。我們看一下另外一個檔案

一文讀懂git核心工作原理
通過cat-file命令的-t選項實際上可以看到這個檔案的型別,它是一個tree物件,表示一個目錄

一文讀懂git核心工作原理
而這個tree物件的內容是其他一些tree和blob物件,也就是這個目錄的子目錄和檔案。當然我們也能在objects資料夾中找到整個倉庫裡所有的commit、tree和blob物件。好了,對.git檔案的初步探祕就先到這裡,我們知道了git是如何儲存所有檔案,以及如何組織各個不同版本的倉庫(或者說是commit)的

手工構造一個commit

看了上文的部分你應該對git的底層儲存有一個大致的瞭解了,不過還不足以讓我們對git底層的工作原理有一個深入的理解。接下來我們通過一些底層操作來手工構造出一個commit,通過這個過程來深入理解git的工作原理。首先我們構造一個空的git倉庫

一文讀懂git核心工作原理
然後我們可以在另一個視窗watch一下.git檔案的變化

一文讀懂git核心工作原理

一文讀懂git核心工作原理
可以看到objects裡面只有info和pack,先忽略這兩個檔案,目前倉庫中還沒有檔案,也沒有commit,接下來我們向倉庫中寫入一些內容,但是不是用直接建立檔案的方式,而是用一些git底層命令,如下

一文讀懂git核心工作原理
首先echo一個"hello world!"字串,然後用管道把stdin也就是標準輸入作為輸入將這個字串傳遞給git的hash-object命令

一文讀懂git核心工作原理
這個是通過git help hash-object檢視到的該命令的文件,通過-w選項,我們可以直接將物件寫入git的物件倉庫,此時我們再看一下.git資料夾發生的變化

一文讀懂git核心工作原理
可以看到objects下已經多了一個檔案,cat看下

一文讀懂git核心工作原理
通過cat命令檢視發現是亂碼,然後我們用cat-file

一文讀懂git核心工作原理
發現它是一個blob物件,我們的確寫入了一個檔案到git倉庫,然後檢視一下檔案內容

一文讀懂git核心工作原理
內容正是我們之前通過echo輸入的字串"hello world!"。好了,這樣我們就將一個檔案寫入到了git倉庫中,這時候用git log一下日誌

一文讀懂git核心工作原理
輸出顯示倉庫中還沒有任何提交,我們再git status一下看看當前倉庫的狀態

一文讀懂git核心工作原理
同樣,啥都沒有。這個時候我們只是通過底層命令直接把檔案寫入了倉庫的資料庫中,而這樣還無法讓git幫我們管理這個檔案。接下來我們需要做什麼呢?回憶一下我們正常寫入一個檔案並交由git管理需要怎麼做?首先建立並編寫一個檔案,然後需要通過git add將該檔案新增到git管理,再通過git commit提交這個檔案到本地倉庫。那我們現在還是用git的底層命令來完成這些操作。接下來需要用到update-index這個命令,git help檢視一下文件

一文讀懂git核心工作原理
可以看到這個命令是用來更新索引檔案,而索引檔案就是我們的stage區,也就是暫存檔案的索引,而git add檔案實際上就是將檔案新增到暫存區並更新了索引檔案。所以接下來我們用這個命令來新增該檔案到暫存區並更新索引檔案

一文讀懂git核心工作原理
--add選項把檔案新增到暫存區,--cacheinfo選項表示直接將資訊寫入索引檔案,100644在unix下表示一個普通檔案(100)和它對應的讀寫許可權(644),同時我們指定了一個名字"test.txt"作為剛才我們輸入的文字"hello world!"的檔名。這時我們在檢視一下倉庫的狀態

一文讀懂git核心工作原理
可以看到我們已經成功建立了test.txt這個檔案,並被git識別到了這個新檔案。接下來我們回到第一張圖

一文讀懂git核心工作原理
可以看到一個commit物件是指向tree物件的,也就是一個目錄,然後tree物件再指向blob物件,也就是一個具體的檔案。我們現在已經成功建立了一個git管理的檔案,則還需要生成tree物件。接下來我們利用另外一個命令write-tree,還是先git help檢視一下

一文讀懂git核心工作原理
可以看到這個命令會建立一個tree物件並寫入索引檔案,接著我們執行這個命令

一文讀懂git核心工作原理
輸出了一個新的hash,也就是git為我們寫入的tree物件生成的hash,cat-file看一下

一文讀懂git核心工作原理

一文讀懂git核心工作原理
沒問題,型別是tree,內容可以看到這個tree物件指向一個blob物件,也就是剛才的test.txt檔案。看一下.git資料夾的變化

一文讀懂git核心工作原理

可以看到objects下多了一個物件,現在總共有兩個物件,一個是之前的test.txt的blob物件,另一個也就是剛才寫入的tree物件。再檢視下倉庫當前的狀態

一文讀懂git核心工作原理
可以看到commit還沒有生成,此時我們已經將tree物件寫入到倉庫,接下來就是寫入commit物件了。寫入commit需要用到commit-tree這個命令,git help一下

一文讀懂git核心工作原理
我們可以通過這個命令將tree物件(在目前的場景下,這個tree物件表示專案根路徑,也就是整個專案)作為一個commit寫入git倉庫

一文讀懂git核心工作原理
從輸出可以看到git又生成了一個hash,這個hash則是生成的commit的hash值。看一下.git資料夾的變化

一文讀懂git核心工作原理
可以看到objects下又多出了一個物件,現在總共有三個物件,一個blob一個tree,還有一個從檔名可以看出就是剛才寫入的commit物件,cat-file檢視一下

一文讀懂git核心工作原理

一文讀懂git核心工作原理
可以看到這個物件型別是commit,而內容可以看到這個commit物件指向的剛才的tree物件,還包含auther、committer、commit message等資訊。

一文讀懂git核心工作原理
這時再git log下,發現還是沒有commit記錄,可是我們已經成功寫入了commit物件,這是為什麼呢?實際上git倉庫在需要維護一個指標,指向某一個commit物件,這樣當一個分支上有很多個提交時,通過這個指標才能確定當前倉庫是哪個版本,也就是哪個commit對應的快照。而這個指標也儲存在.git檔案中

一文讀懂git核心工作原理
也就是這個HEAD檔案。cat看一下

一文讀懂git核心工作原理
HEAD指標指向了refs/heads/master這個檔案,那檢視下這個檔案

一文讀懂git核心工作原理
現在refs/heads這個檔案還是空的,所以我們現在要寫入一個檔案,使HEAD指標指向我們剛才建立的commit物件。這裡有兩種方式,一種是直接通過建立檔案並編輯寫入,不過不推薦用這種方式直接修改HEAD指標的指向,於是我們用另一種方式,利用update-ref命令更新一下HEAD指標的指向,使它指向之前生成的commit物件

一文讀懂git核心工作原理

一文讀懂git核心工作原理
可以看到refs/heads下生成了一個master檔案,cat一下

一文讀懂git核心工作原理
master檔案的內容就是剛才生成的commit的hash值(master實際上就是當前的分支,也就是git倉庫預設的master分支)。再git log看下

一文讀懂git核心工作原理
好了,commit已經成功生成了。再git status看下

一文讀懂git核心工作原理
顯示test.txt被刪除了,這是因為我們是通過底層命令直接寫入了檔案,而工作區沒有這個檔案。checkout一下就好了

一文讀懂git核心工作原理
再git status就沒問題了。大功告成!

總結

通過上面的一系列講解和一次手工生成commit的過程,應該能對git的底層實現有一個比較深入的理解了。總結一下,實質上git使用了一個“萬物皆物件”的工作模式,通過用commit物件包含tree目錄物件,tree目錄物件包含blob檔案物件來進行整個倉庫的資料夾、檔案和不同版本倉庫的控制和管理,然後再利用指向commit的指標移動來獲取不同的版本快照。這就是git底層最核心的工作原理,理解這個原理後,所有的git命令、操作,包括複雜的工作流、各種場景的實踐和疑難問題的解決都可以從原理出發,抽絲剝繭,事半功倍!

相關文章