一篇文章看懂Git的內部存儲結構
Git是一個開源的分布式版本控制系統,目前大多數團隊都采用Git作為團隊內部的版本控制系統,并且在DevOps的整體流程中,版本控制是其中重要的一環,在“基礎設施即代碼”的理念下,不止是系統源代碼,系統配置納入版本管理,數據和環境配置也要納入版本管理,可以說,版本管理使得研發流程可復用、標準化、自動化,進而提高了研發效率。
大多數人在使用git時,大多數都是使用IDE封裝好的GUI界面操作,少量使用者也是git add,git commit,git pull幾個常用的命令。對于git的用戶來說,這些都足夠了。如果你是一個DevOps開發者或是想成為Git的深度用戶,知道這些是遠遠不夠的。下面就讓我們了解一下Git背后的邏輯。
第一、Git的三個區+一個遠程倉庫

這個圖好多人都應該見過。里面也是一些經常使用的命令。主要包含幾個部分:
①Remote:遠程倉庫,像github就是一個遠程倉庫。
②Repository:本地倉庫,通過git clone將遠程倉庫的代碼下載到本地。代碼庫的元數據信息在根目錄下的.git目錄下。
③Workspace:工作空間,就是我們寫代碼的目錄。
④Index:暫存區,指的是.git目錄下的index文件。
在平時寫完代碼后執行git add 就是將變更的內容從工作空間提交到暫存區,git commit就是將暫存區的內容提交到本地代碼庫里,git push 就是將本地代碼庫的變更提交到遠程倉庫,這時其他人就能通過pull 將你的變更下載到工作空間。
第二、git的內部存儲結構
新建一個gittest目錄,然后執行git init就會初始化一個本地倉庫。打開.git目錄文件列表如下:

hooks:是存儲git鉤子的目錄,鉤子是在特定事件發生時觸發的腳本。比如:提交之前,提交之后。
info:是存儲git信息的目錄,比如排除特定后綴的文件.
objects:是存儲git各種對象及內容的對象庫,包含正常的和壓縮后的。
refs:是存儲git各種引用的目錄,包含分支、遠程分支和標簽。
config:是代碼庫級別的配置文件。
HEAD:是代碼庫當前指向的分支,這里為master。
一、新建README.md文件
新建README.md文件,并輸入內容“This is test file!”

git add README.md 將工作空間的變更提交到暫存區。
.git/index文件會被修改,通過git ls-files --stage 查看暫存區的內容,可以看到README.md文件有修改,通過git cat-file -p SHA-1查看文件內容。

同時.git/objects下也會有新的對象,如下:

這個對象就是剛才add到暫存區的對象,在.git/objects目錄下看到一個文件。這便是Git存儲數據內容的方式--為每份內容生成一個文件,取該內容與頭信息的SHA-1校驗和,創建以該校驗和前兩個字符為名稱得子目錄,并以校驗和剩下38個字符為文件命名。這里并沒有顯示真實的文件名。
通過git cat-file -t SHA-1查看對象的類型。blob代表文件。

二、創建一個目錄web,并添加一個web.txt文件

執行git add web/ 添加到暫存區。
查看暫存區內容,變成了2條記錄,存儲方式和上面文件一樣。

可以看出,這里只有文件內容生成的文件,沒有為目錄生成文件。
在.git/objects下面也創建了相應的目錄和文件。

三、提交內容
到目前為止,暫存區的內容有README.md和web/web.txt文件。下面通過git commit 將暫存區的內容提交到本地倉庫。

使用git log 查看本次提交信息,使用git cat-file -p查看變更內容,當類型為tree時表示文件夾,會顯示該文件夾下的文件或目錄列表,當類型為blob時為文件,會顯示該文件的內容。

因此本次commit的存儲模型為:

此時再查看.git/objects目錄下,有增加了幾個目錄。其中:
8d:提交的commit對象
58和ff:提交的commit對應的tree對象和web目錄的tree對象。
a1和b3:提交的README.md文件和web/web.txt文件對應的數據對象。

可以看出,當我們執行git add 和git commit 命令時,Git做的工作是將被改寫的文件保存為數據對象、更新暫存區,記錄樹對象,最后創建一個指明了頂層樹對象和父提交的提交對象。這三種Git對象(數據對象-blob、樹對象-tree、提交對象-commit)最后均以單獨文件的形式保存在.git/objects目錄下。
四、新建分支test
上面提到分支、遠程分支和標簽都會存儲在.git/refs下。

heads包含分支,tags包含標簽。每個引用文件里都會指向一個commit,如基于master分支新建的test分支 git branch test:

使用git checkout test切換到test分支,此時HEAD的內容為refs/heads/test,表示當前分支為test.

在test分支下修改文件內容:

git status查看工作空間狀態,有兩個修改過的文件。

git add .將變更的文件添加到暫存區,查看暫存區內容發現:
README.md的校驗和從之前的a177f89--->7a51843
web/web.txt的校驗和從之前的b31494b--->f5ebb2c
查看文件內容也是最新修改的內容。

.git/objects目錄下也多了2個新的目錄

git commit 將暫存區的內容提交到本地倉庫。和上面一樣,此時也會創建提交對象,樹對象,通過查看不同的對象,最后都能查看到具體的數據對象的最新內容。

同時在.git/objects下也會創建提交對象c7,樹對象cf和樹對象a3

五、分支合并沖突
在master分支修改web/web.txt,將內容改為“this is a old web.txt”,以便產生沖突。
git add 添加到暫存區,查看文件內為新修改的內容。

git commit添加到本地倉庫,同時生成提交對象,樹對象。

執行git merge test進行分支合并,web/web.txt出現沖突。

分支合并后,master分支的commit由之前的 8da82ba變為c75be2f。
暫存區中的README.md的校驗和也由master分支的 a177f89變成7a51843。
而web/web.txt變成了3個文件,b3是master當前commit的父commit的校驗和,18是master最新提交的校驗和,f5是test分支上的校驗和。

解決沖突,兩行內容都保留,結果如下:

git add 添加到暫存區,查看暫存區內容,README.md是test分支上的,web/web.txt是解決完沖突新生成的校驗和。

也就是說,分支合并會修改當前分支的commit信息以及暫存區的內容,當解決完沖突后,將沖突文件git add 添加到暫存區,然后git commit 將合并后的內容提交到本地倉庫。


也就是說,合并完沖突后,只有將更新后的內容commit,在生成的提交對象上才能找到新加的內容。
這里回答下遇到的一個問題:
問題:我當前分支是test分支,當從master分支合并過來后沖突了,我解決完沖突,只提交了這個文件,其他文件不用提,因為其他文件的內容已經在master分支上了,當我從test分支再合并回master分支后,也應該沒有問題。
解釋:根據上面的分析,每個commit都是一個提交對象,這個提交對象關聯一個樹對象,樹對象會包含最新的數據對象。當從master分支合并后,master的commit提交對象包含的樹對象和數據對象,會作為新的文件內容存放到test分支的暫存區,此時有2種選擇:
①,當在test分支commit時,這次生成的提交對象才會包含master過來的文件內容。
②,如果此時放棄提交(手動checkout當前分支最新的),那么當前commit提交對象不包含master的文件內容,當test分支合并回master時,也會將test分支的文件內容存放到master分支的暫存區,當push到遠程倉庫時,也就是test分支的數據對象的內容,因此其他人拿到的文件內容就會丟失。