Git管理文件的原理分析以及Git的樹對象

我們知道Git與SVN有着很多區別。Git相比SVN更加高效,其中主要的原因就是它把文件內容按元數據形式存儲,可以理解爲存到了一種類似K/V型的數據庫裏。

image-20200222075617191那麼我們來分析下,它到底是如何存儲文件以及如何管理提交與回滾的。


1.基礎環境準備

在當前目錄初始化一個用於測試的Git倉庫git_test_01

$ git init git_test_01;cd git_test_01;

創建一個文件並寫入內容

$ echo 'first line' > test-file-01.txt;

添加到暫存區並且提交該文件

$ git add -A;git commit -am "first commit";

使用git log --pretty=oneline查看提交

image-20200222083157204

如此,我們便成功的提交了一個文件。那麼讓我們進入**.git目錄下的objects**文件夾看看發生了什麼。

image-20200222083616879

我們發現這裏有個d8開頭的目錄,與我們上次提交後產生的hash碼的開頭前兩位是一樣的。

我們使用命令ls -l d8看看它究竟有什麼

image-20200222084000069

這裏是一個名稱爲85f1211e0cd1930bfdeecda5ac85998639f7d5的文件,我們發現將d8和這個文件名組合一下居然和上面的提交id是一樣的。這兩者有什麼關聯呢?


2.使用Git命令查看提交內容

2.1 內容寫入及讀取

在探究上面使用git 的 commit後生成上述目錄及文件的原理之前,我們先看看如何使用git命令將內容寫入到上面我們提到的K/V數據庫裏。

$ echo 'first write content to object' | git hash-object -w --stdin;

image-20200222085717811

返回給我們一個hash值7163feb56727aea8393398aac17ec94e037da1b8;

既然是一個K/V型數據庫,那麼使用該key應該可以獲取到value纔對。git給我們提供這樣一個命令

$ git cat-file -p 7163feb56727aea8393398aac17ec94e037da1b8;

image-20200222090125894

我們發現使用該命令根據key(也就是哈希碼)獲取到了文件內容。


接着我們使用命令查看一下有多少個提交文件。

find .git/objects/ -type f;

image-20200222090639792

我們發現總共有四個結果。其中最後一個就是我們這次用git hash-object -w --stdin寫入後返回的key值。我們還看到了前面提交的d885f1211e0cd1930bfdeecda5ac85998639f7d5,至於爲什麼還會產生其它兩個我們暫時不管,後面將揭曉。


那按照上面的結果,我們是不是可以猜測下,當我們每次寫入一個新的不同內容到對象裏時,都會產生一個結果呢?

image-20200222091601482

結果確實如我們所料,當我們的second和third的內容寫入後,確實生成了不同的提交,返回了不同的key。但是大家有沒有發現,我最後一次操作,所 寫入的內容和我們第一次寫入的內容是一致的,都是first。但是它是沒有生成不同的key的,沒有產生新的文件。你會發現它仍然和我們提交的第一次返回的7163feb56727aea8393398aac17ec94e037da1b8是一個值。

到此,我們可以做個小總結。Git會對你提交的內容進行一個運算,返回一個基於內容的唯一key值,根據這個key值你可以獲取到相關的內容。並且如果你提交的內容前後是一樣的,git計算出的key值自然也是和上次是一樣的。所以從這點可以看出,Git是一個基於內容的文件尋址系統

上述如果將 echo結果輸出到一個文件中,那麼當每次add文件時其實就相當於執行了git hash-object -w --stdin命令,將修改後的內容存儲到數據庫中。

image-20200222093306636


2.2 內容回滾指定版本

假設我們需要回滾某個文件的版本,只需要取出指定key的內容重新寫入到這個文件中就行了,是不是有點感覺了?

比如我們要回滾second的內容

$ git cat-file -p 53082820b5e5 > test-file-01.txt;

image-20200222093637219

注意提交的key不需要全部,只截取開頭的部分也是可以的,只要能保證唯一性。

可以發現我們的文件內容已經被更新了!由此我們思考到,每次我們修改文件後,如果前後內容不一致,就產生了一個文件(其實是blob類型)。當我們需要回到該文件的某個版本時,再根據key讀取出內容重新寫入即可。

image-20200222094412515

但是你是否有個疑問,我一次提交可能提交多個文件,多個文件夾(目錄),Git是怎麼知道每次提交的各個文件名的呢?


3.Git的樹對象

Git採用樹對象來解決文件名的問題。一個樹對象中包含了多個文件名稱與其對應的key,還有其它樹對象的引用。

我們現在來驗證這個樹對象的存在。

由於上面的測試我們將git_test_01文件內容寫爲了second write content to object,我們先提交本次修改。

$ git add -A;git commit -am'second commit';

現在,讓我們在當前目錄新增一個 文件git_test_02.txt,再新增一個文件 /a/b/git_test_0.3.txt

$ echo 'second file' > git_test_02.txt;
$ mkdir -p ./a/b; echo 'third file' > ./a/b/git_test_03.txt;

最後執行提交。

我們使用命令git cat-file -p master^{tree}查看分支樹

image-20200222100815028

然後使用命令git log --pretty=oneline再次查看提交歷史

image-20200222101014384

接着使用命令git cat-file -p查看當前最新的提交

image-20200222101531409

可以發現對git log中最新一次提交的key 查看後,內容包括瞭如下幾部分

  • tree-本次提交的頂級樹對象
  • parent-上一次提交的key(通過類似指針/引用 的方式指向上一次提交)
  • author-提交者
  • comitter-郵箱等信息
  • 最下方的提交內容

那麼我們接着查看這個tree對象的key對象的內容

image-20200222102011679

再和上面我們查看的提交對象的內容比對下

image-20200222100815028

你會發現,驚人的一樣!

這就解釋了爲什麼我們當前分支的各個文件及目錄的版本信息,和我們當前所處提交的版本信息是一致的,且記錄着各個文件、文件名、目錄。

你會發現,我們的a目錄成爲了一個tree對象被包含在了當前的頂級樹對象中。我們的文件成爲了blob類型的元數據文件。

最後的結構如下圖:

提交對象1


假設我們此時要修改文件,git_test_03.txt的內容,然後再做一次提交。

image-20200222131151533

image-20200222131524501

我們對本次提交進行樹對象的分析,得到如下的結構圖:

提交對象3

比較兩個樹,我們用紅色標註了發生變化的key值情況,並且下面逐一解析爲什麼會發生變化。

  • parent發生變化,指向了上次提交的key,也就是提交對象1
  • git_test_03.txt文件內容發生變化,當提交後產生新的key
  • 因爲b裏面存的是git_test_03.txt的key,該key發生了變化,因此b本身也會變化
  • 因爲a裏面存的是b的key,因爲b的key發生了變化,因此a本身也發生了變化
  • 頂級tree包含了a,git_test_02.txt,test-file-01.txt,雖然兩個文件沒發生變化,但是a發生了變化,因此頂級tree也變化

小結:

當commit後,至少會生成當前提交對象,生成頂級樹對象。以及該樹對象包含的文件、子樹對象等。

其實這棵樹的存儲方式類似於鏈表,存儲的是就是指針/引用的概念,也就是key,git根據該key就可以獲取到value。

當每次回滾,代碼切換的時候,只需要比對樹即可更新相應的文件內容!如果文件內容沒變的話,對應的key也不會變化,也就無需重寫等操作了。因此git的效率是非常高的,因爲它只需要比對樹對象裏各個key值即可。


4.分支是什麼?

我們創建一個新分支develop

$ git checkout -b develop;

進入到.git/refs/heads目錄下,並查看文件

image-20200222142434657

比對兩個文件的內容

image-20200222142522766

發現內容都是一樣的,保存的是當前提交對象的key值。那麼根據這個key值就可以得到完整的提交信息以及頂級樹對象了。也就能獲取到各個文件的key了,根據key又可以獲取文件內容了!

這其實就是git分支的本質,其實就是記錄了一次提交的key值!

上述如果我們develop和master不同自然兩個文件裏記錄的也是不一樣的提交對象key值!

同理,我們可以推導出標籤🏷 tag也是一個道理,只不過它不在heads下,而是在tags下。大家可以自己試驗一下


5.總結

本篇博客分享了個人對於Git原理的學習心得,如果有不當之處希望積極指出,這可以幫助我糾正不足同時也能讓更多的人有一定的收穫,避免踩坑~謝謝大家,後續會繼續分享學習心得。最後附一下Git基礎命令腦圖

Git

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章