我們知道Git與SVN有着很多區別。Git相比SVN更加高效,其中主要的原因就是它把文件內容按元數據形式存儲,可以理解爲存到了一種類似K/V型的數據庫裏。
那麼我們來分析下,它到底是如何存儲文件以及如何管理提交與回滾的。
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
查看提交
如此,我們便成功的提交了一個文件。那麼讓我們進入**.git目錄下的objects**文件夾看看發生了什麼。
我們發現這裏有個d8
開頭的目錄,與我們上次提交後產生的hash碼的開頭前兩位是一樣的。
我們使用命令ls -l d8
看看它究竟有什麼
這裏是一個名稱爲85f1211e0cd1930bfdeecda5ac85998639f7d5
的文件,我們發現將d8
和這個文件名組合一下居然和上面的提交id是一樣的。這兩者有什麼關聯呢?
2.使用Git命令查看提交內容
2.1 內容寫入及讀取
在探究上面使用git 的 commit後生成上述目錄及文件的原理之前,我們先看看如何使用git命令將內容寫入到上面我們提到的K/V數據庫裏。
$ echo 'first write content to object' | git hash-object -w --stdin;
返回給我們一個hash值7163feb56727aea8393398aac17ec94e037da1b8
;
既然是一個K/V型數據庫,那麼使用該key應該可以獲取到value纔對。git給我們提供這樣一個命令
$ git cat-file -p 7163feb56727aea8393398aac17ec94e037da1b8;
我們發現使用該命令根據key(也就是哈希碼)獲取到了文件內容。
接着我們使用命令查看一下有多少個提交文件。
find .git/objects/ -type f;
我們發現總共有四個結果。其中最後一個就是我們這次用git hash-object -w --stdin
寫入後返回的key值。我們還看到了前面提交的d885f1211e0cd1930bfdeecda5ac85998639f7d5
,至於爲什麼還會產生其它兩個我們暫時不管,後面將揭曉。
那按照上面的結果,我們是不是可以猜測下,當我們每次寫入一個新的不同內容到對象裏時,都會產生一個結果呢?
結果確實如我們所料,當我們的second和third的內容寫入後,確實生成了不同的提交,返回了不同的key。但是大家有沒有發現,我最後一次操作,所 寫入的內容和我們第一次寫入的內容是一致的,都是first。但是它是沒有生成不同的key的,沒有產生新的文件。你會發現它仍然和我們提交的第一次返回的7163feb56727aea8393398aac17ec94e037da1b8
是一個值。
到此,我們可以做個小總結。Git會對你提交的內容進行一個運算,返回一個基於內容的唯一key值,根據這個key值你可以獲取到相關的內容。並且如果你提交的內容前後是一樣的,git計算出的key值自然也是和上次是一樣的。所以從這點可以看出,Git是一個基於內容的文件尋址系統。
上述如果將 echo結果輸出到一個文件中,那麼當每次add文件時其實就相當於執行了git hash-object -w --stdin
命令,將修改後的內容存儲到數據庫中。
2.2 內容回滾指定版本
假設我們需要回滾某個文件的版本,只需要取出指定key的內容重新寫入到這個文件中就行了,是不是有點感覺了?
比如我們要回滾second的內容
$ git cat-file -p 53082820b5e5 > test-file-01.txt;
注意提交的key不需要全部,只截取開頭的部分也是可以的,只要能保證唯一性。
可以發現我們的文件內容已經被更新了!由此我們思考到,每次我們修改文件後,如果前後內容不一致,就產生了一個文件(其實是blob類型)。當我們需要回到該文件的某個版本時,再根據key讀取出內容重新寫入即可。
但是你是否有個疑問,我一次提交可能提交多個文件,多個文件夾(目錄),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}
查看分支樹
然後使用命令git log --pretty=oneline
再次查看提交歷史
接着使用命令git cat-file -p
查看當前最新的提交
可以發現對git log中最新一次提交的key 查看後,內容包括瞭如下幾部分
- tree-本次提交的頂級樹對象
- parent-上一次提交的key(通過類似指針/引用 的方式指向上一次提交)
- author-提交者
- comitter-郵箱等信息
- 最下方的提交內容
那麼我們接着查看這個tree對象的key對象的內容
再和上面我們查看的提交對象的內容比對下
你會發現,驚人的一樣!
這就解釋了爲什麼我們當前分支的各個文件及目錄的版本信息,和我們當前所處提交的版本信息是一致的,且記錄着各個文件、文件名、目錄。
你會發現,我們的a目錄成爲了一個tree對象被包含在了當前的頂級樹對象中。我們的文件成爲了blob類型的元數據文件。
最後的結構如下圖:
假設我們此時要修改文件,git_test_03.txt的內容,然後再做一次提交。
我們對本次提交進行樹對象的分析,得到如下的結構圖:
比較兩個樹,我們用紅色標註了發生變化的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
目錄下,並查看文件
比對兩個文件的內容
發現內容都是一樣的,保存的是當前提交對象的key值。那麼根據這個key值就可以得到完整的提交信息以及頂級樹對象了。也就能獲取到各個文件的key了,根據key又可以獲取文件內容了!
這其實就是git分支的本質,其實就是記錄了一次提交的key值!
上述如果我們develop和master不同自然兩個文件裏記錄的也是不一樣的提交對象key值!
同理,我們可以推導出標籤🏷 tag也是一個道理,只不過它不在heads下,而是在tags下。大家可以自己試驗一下
5.總結
本篇博客分享了個人對於Git原理的學習心得,如果有不當之處希望積極指出,這可以幫助我糾正不足同時也能讓更多的人有一定的收穫,避免踩坑~謝謝大家,後續會繼續分享學習心得。最後附一下Git基礎命令腦圖