一文讀懂git核心工做原理

寫在前面

你是否也常常學習git,而後發現仍是沒法徹底掌握它的使用?曾經我也感受學習了不少git的使用技巧,查閱了不少文檔和教程,但當碰到不少場景和問題時仍是一臉茫然。直到我深刻學習了git的工做原理,再去結合實踐中的場景和問題後,才慢慢有了一種融會貫通的感受。當理解了問題的原理後,不論是使用仍是解決實踐中的問題,都會變得遊刃有餘git

git原理剖析

commit究竟是什麼

先來看一張圖數據庫

其實git內部一切皆對象,commit對象是整個git倉庫的一個快照,包含了整個倉庫全部的數據,從圖中能夠看到一個commit對象在git內部實際上指向一個tree對象,這個tree是項目的根路徑,而後遞歸指向下面的tree和blob對象,blob則是單個文件的二進制存儲。有人可能會有疑惑,若是每一個commit都包含整個倉庫,那若是兩個commit之間只修改了一個文件,那其餘全部文件不是都冗餘存儲了嗎?實際上固然不會,再來看一張圖學習

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

探祕.git文件

你有沒有想過,爲何當你從git倉庫中clone一個項目後,就能夠進行各類git操做來進行團隊協做了?git其實也是計算機上的一個程序,加上它所須要的數據就能開始工做了,而這些數據所有都存放在項目的.git文件中。下圖是一個普通git項目的.git文件夾unix

咱們先重點關注下這個object文件夾,cd進去

能夠看到一堆兩個字符的文件夾,隨便找一個cd進去(我找了00這個文件夾)

此次是一些一長串字符做爲文件名的文件(實際上剛纔的上層目錄名00加上當前目錄的一個文件名就獲得了git中的一個對象,git用一個40個字符的hash來表示一個對象,它多是commit、tree或者是blob),咱們用cat命令查看一下文件的內容

能夠看到結果是亂碼,實際上它是一個blob對象,也就是二進制存儲的文件,這裏咱們能夠用另一個git命令,cat-file來查看一下它轉化後的內容

這個實際上就是一個倉庫裏某一個版本中,也就是某一次commit中的一個文件的具體內容。咱們看一下另一個文件

經過cat-file命令的-t選項實際上能夠看到這個文件的類型,它是一個tree對象

而這個tree對象的內容是其餘一些tree和blob對象,也就是這個目錄的子目錄和文件。固然咱們也能在objects文件夾中找到整個倉庫裏全部的commit、tree和blob對象。好了,對.git文件的初步探祕就先到這裏,咱們直到了git是如何存儲全部文件,以及如何組織各個不一樣版本的倉庫(或者說是一個commit)的

手工構造一個commit

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

而後咱們watch一下.git文件的變化

能夠看到objects裏面只有info和pack,先忽略這兩個文件,目前倉庫中尚未文件,也沒有commit,接下來咱們向倉庫中寫入一些內容,可是不是用直接建立文件的方式,而是用一些git底層命令,以下

首先echo一個"hello world!"字符串,而後用管道把stdin也就是標準輸入做爲輸入將這個字符串傳遞給git的hash-object命令

這個是經過git help hash-object查看到的該命令的文檔,能夠看到經過-w選項,咱們能夠直接將對象寫入git的對象倉庫,此時咱們再看一下.git文件夾發生的變化

能夠看到objects下已經多了一個文件,咱們cd進去看看

經過cat命令查看發現是亂碼,而後咱們用cat-file

發現它是一個blob對象,咱們的確寫入了一個文件到git倉庫,而後查看一下文件內容

內容正是咱們以前經過echo輸入的字符串"hello world!"。好了,這樣咱們就將一個文件寫入到了git倉庫中,這時候用git log一下日誌

輸出顯示倉庫中尚未任何提交,咱們再git status一下看看當前倉庫的狀態

一樣,啥都沒有。這個時候咱們只是經過底層命令直接把文件寫入了倉庫的數據庫中,而這樣還沒法讓git幫咱們管理這個文件。接下來咱們須要作什麼呢?回憶一下咱們正常寫入一個文件並交由git管理須要怎麼作?首先建立並編寫一個文件,而後須要經過git add將該文件添加到git管理,再經過git commit提交這個文件到本地倉庫。那咱們如今仍是用git的底層命令來完成這些操做。接下來須要用到update-index這個命令,git help查看一下文檔

能夠看到這個命令是用來更新索引文件,而索引文件就是咱們的stage區,也就是暫存文件的索引,而git add文件實際上就是將文件添加到暫存區並更新了索引文件。因此接下來咱們用這個命令來添加該文件到git管理並更新索引文件

--add選項把文件添加到暫存區,--cacheinfo選項表示直接將信息寫入索引文件,100644在unix下表示一個普通文件和它對應的讀寫權限(644),同時咱們指定了一個名字"test.txt"做爲剛纔咱們輸入的文本"hello world!"的文件名。這時咱們在查看一下倉庫的狀態

能夠看到咱們已經成功建立了test.txt這個文件,並被git識別到了這個新文件。接下來咱們回到第一張圖

能夠看到一個commit是指向tree對象的,而後tree對象再指向blob對象,也就是一個具體的文件。咱們如今已經成功建立了一個git管理的文件,則還須要生成tree對象。接下來咱們利用另一個命令write-tree,仍是先git help查看一下

能夠看到這個命令會建立一個tree對象並寫入索引文件,接着咱們執行這個命令

輸出了一個新的hash,也就是git爲咱們寫入的tree對象生成的hash,cat-file看一下

沒問題,類型是tree,內容能夠看到這個tree對象指向一個blob對象,也就是剛纔的test.txt文件。看一下.git文件夾的變化

能夠看到objects下多了一個對象,如今總共有兩個對象,一個是以前的test.txt的blob對象,另外一個也就是剛纔寫入的tree對象。再查看下倉庫當前的狀態日誌

能夠看到commit尚未生成,此時咱們已經將tree對象寫入到倉庫,接下來就是寫入commit對象了。寫入commit須要用到commit-tree這個命令,git help一下

咱們能夠經過這個命令將tree對象(在目前的場景下,這個tree對象表示項目根路徑,也就是整個項目)做爲一個commit寫入git倉庫

從輸出能夠看到git又生成一個hash,這個hash則是生成的commit的hash值。看一下.git文件夾的變化

能夠看到objects下又多出了一個對象,如今總共有三個對象,一個blob一個tree,還有一個從文件名能夠看出就是剛纔寫入的commit對象,cat-file查看一下

能夠看到這個對象類型是commit,而內容能夠看到這個commit對象指向的剛纔的tree對象,還包含auther、committer、commit message等信息。

這時再git log下,發現仍是沒有commit記錄,但是咱們已經成功寫入了commit對象,這是爲何呢?實際上git倉庫在須要維護一個指針,指向某一個commit對象,這樣當一個分支上有不少個提交時,經過這個指針才能肯定當前倉庫是哪一個commit指向的快照。而這個指針也存儲在.git文件中

也就是這個HEAD文件。cat看一下

HEAD指針指向了refs/heads/master這個文件,那查看下這個文件

如今refs/heads這個文件仍是空的,因此咱們如今要寫入一個HEAD指針指向commit對象到這個文件。有兩種方式,一種是直接經過建立文件並編輯寫入,不過不推薦用這種方式直接修改HEAD指針的指向,因而咱們用另外一種方式,利用update-ref命令更新一下HEAD指針的指向,使它指向以前生成的commit對象

能夠看到refs/heads下生成了一個master文件,cat一下

master文件的內容就是剛纔生成的commit的hash值(master實際上就是當前的分支,也就是git倉庫默認的master分支)。再git log看下

好了,commit已經成功生成了。再git status看下

顯示test.txt被刪除了,這是由於咱們是經過底層命令直接寫入文件的關係。checkout一下就行了

再git status就沒問題了。大功告成!

總結

經過上面的一系列講解和一次手工生成commit的過程,應該能對git的底層實現有一個比較深刻的理解了。總結一下,實質上git使用了一個「萬物皆對象」的工做模式,經過用commit對象包含tree目錄對象,tree目錄對象包含blob文件對象來進行整個倉庫的文件夾、文件和不一樣版本倉庫的控制和管理,而後再利用指向commit的指針移動來獲取不一樣的版本快照。這就是git底層最核心的工做原理,理解這個原理後,全部的git命令、操做,包括複雜的工做流、各類場景的實踐和疑難問題的解決均可以從原理出發,抽絲剝繭,事半功倍!cdn

相關文章
相關標籤/搜索