Git 實現(xiàn)原理,你真的了解嗎?
誕生的原因
最初版本的git 誕生于Linus和他的團隊在開發(fā)Linux內(nèi)核的過程中。一開始,Linus和他的團隊選用的版本控制工具是BitKeeper(一款非開源,但是有條件免費的產(chǎn)品)。但后來由于種種原因BitKeeper宣布終止免費,當(dāng)時市面上也沒有能滿足Linux內(nèi)核開發(fā)所需要的分布式版本控制系統(tǒng),Linus為了解決這一問題,閉關(guān)一周,獨自設(shè)計編寫了初版git。
名字含義
Linus本人對git這一名字的解讀是愚蠢的內(nèi)容追蹤器(the stupid content tracker),git在俚語中表示的是愚蠢這一類的貶義詞。比較受人歡迎的解釋應(yīng)該是全局信息追蹤器(Global Information Tracker)。
Git實現(xiàn)原理
初始化一個git倉庫,此時文件夾中只有一個隱藏文件夾 .git , .git 文件夾為git的版本庫,存放git實現(xiàn)版本控制所需要的全部信息。使用 find .git 命令查看 .git 中存儲的內(nèi)容如下:

下文將圍繞上圖中的文件目錄展開介紹(由于倉庫初始化,index文件,COMMIT_EDITMSG文件以及l(fā)ogs文件夾等沒有在上圖中顯示)。
config
config文件記錄項目的配置,如下圖:

更改配置時,也可以直接對config文件中的內(nèi)容進行修改。如果某個git倉庫中的config文件配置與全局配置沖突,以該倉庫配置文件為準(zhǔn)。例:

objects(對象庫)
對象庫(objects)中包含該倉庫的原始文件數(shù)據(jù)。
可尋址內(nèi)容名稱
Git對象庫被組織及實現(xiàn)成一個內(nèi)容尋址的存儲系統(tǒng)。具體而言,對象庫中的每個對象都有一個唯一的名稱,這個名稱是向?qū)ο蟮膬?nèi)容應(yīng)用SHA1得到的SHA1散列值(一個對象的完整內(nèi)容決定了這個散列值)。
實例展示:
初始時,倉庫中有已提交的 file01.txt 以及 file02.txt 文件:

根據(jù)類型來分,對象庫中的數(shù)據(jù)可以分為以下三類:
塊(blob)
git倉庫中的每個文件的每一個版本表示為一個塊。每個塊被視為一個黑盒。一個塊保存一個文件的數(shù)據(jù),但不包含任何關(guān)于這個文件的元數(shù)據(jù),甚至連文件名也沒有。如下圖:

目錄樹(tree)
一個目錄樹對象代表一層目錄信息。它記錄blob標(biāo)識符、路徑名和在一個目錄里所有文件的一些元數(shù)據(jù)。它也可以遞歸引用其他目錄樹或子樹對象,從而建立一個包含文件和子目錄的完整層次結(jié)構(gòu)。如下圖:

提交(commit)
一個提交對象保存版本庫中每一次變化的元數(shù)據(jù),包括作者、提交者、提交日期和日志消息。每一個提交對象指向一個目錄樹對象。如下圖:

上圖展示的例子中,提交為master分支的首次提交,對于非首次提交,commit對象還會記錄parent信息,即本次commit所對應(yīng)的上一次提交。三者關(guān)系對應(yīng)圖示如下:

git追蹤內(nèi)容
當(dāng)Git放置一個文件到對象庫中的時候,它基于文件內(nèi)容計算散列值而不是文件名。事實上,Git并不追蹤那些與文件次相關(guān)的文件名或者目錄名。如果兩個文件的內(nèi)容完全一樣,無論是否在相同的目錄,Git在對象庫里只保存一份blob形式的內(nèi)容副本。Git僅根據(jù)文件內(nèi)容來計算每一個文件的散列碼,如果文件有相同的SHA1值,它們的內(nèi)容就是相同的,然后將這個blob對象放到對象庫里,并以SHA1值作為索引。項目中的這兩個文件,不管它們在用戶的目錄結(jié)構(gòu)中處于什么位置,都使用相同的對象指代其內(nèi)容。
實例展示:
初始時,倉庫中只有一個已提交的 file01.txt 文件:

上述過程的圖示變化如下:

如果這些文件中的一個發(fā)生了變化,Git會為它計算一個新的SHA1值,識別出它現(xiàn)在是一個不同的blob對象,然后把這個新的blob加到對象庫里。原來的blob在對象庫里保持不變,給沒有變化的文件使用。其次,當(dāng)文件從一個版本變到下一個版本的時候,Git的內(nèi)部數(shù)據(jù)庫有效地存儲每個文件的每個版本,而不是它們的差異。因為Git使用一個文件的全部內(nèi)容的散列值作為文件名,所以它必須對每個文件的完整副本進行操作。
存儲機制
Git使用了一種叫做打包文件(packfile)的存儲機制。當(dāng)版本庫中有太多的松散對象(Git 最初向磁盤中存儲對象時所使用的格式被稱為“松散(loose)”對象格式),或者你手動執(zhí)行 git gc 命令,以及你向遠程服務(wù)器push時,Git 都會將這些松散對象打包成一個稱為“包文件(packfile)”的二進制文件。要創(chuàng)建一個打包文件,Git首先定位內(nèi)容非常相似的全部文件,然后為它們之一存儲整個內(nèi)容。之后計算相似文件之間的差異并且只存儲差異。因為Git是由文件內(nèi)容驅(qū)動的,所以它并不關(guān)心計算出來的兩個文件之間的差異是否屬于同一個文件的兩個版本。也就是說,Git可以在版本庫里的任何地方取出兩個文件并計算差異,只要它認為它們足夠相似來產(chǎn)生良好的數(shù)據(jù)壓縮。(Git有一套相當(dāng)復(fù)雜的算法來定位和匹配版本庫中內(nèi)容相似的文件)
實例展示:
初始時,倉庫中只有一個已提交的大小為12215字節(jié)的 file01.txt 文件:

在打包前,objects/pack和objects/info文件夾為空,文件以松散對象的形式存儲在objects目錄下。打包后,文件全部打包到 objects/pack 中。pack后綴文件存儲對象文件,idx后綴文件是索引文件,用于允許它們被隨機訪問。objects/info 文件夾記錄對象存儲的附加信息,info/pack文件中記錄打包文件的文件名(上圖中為:P pack-c4750e76b36c227...idx)。
index(索引)
了解索引之前先說一下git的三個工作區(qū)域以及git倉庫中文件的三個狀態(tài)。
三個工作區(qū)域
1. 版本庫(repository)
即上文中提到的 .git 文件夾。
2. 工作目錄(working directory)
工作區(qū)即當(dāng)前分支所對應(yīng)的文件目錄。
3. 暫存區(qū)(staging area)
在工作目錄與倉庫之間還有一個暫存區(qū)。當(dāng)我們在工作目錄中對文件進行增刪改等操作時,這些被修改的內(nèi)容在 git add 命令之后,就會被添加到暫存區(qū)中。git commit 之后,將暫存區(qū)中的內(nèi)容添加到版本庫中(進入對象庫中)。三個工作區(qū)域交互如下圖:

文件分類
Git將所有文件分成3類:已追蹤的、被忽略的以及未追蹤的。
1. 已追蹤的(Tracked)
已追蹤的文件是指已經(jīng)在版本庫,或者是在暫存區(qū)中的文件。
2. 被忽略的(Ignored)
被忽略的文件是指在工作目錄中出現(xiàn),但git并不記錄該文件(夾)的變動。被忽略的文件(夾)必須在版本庫中被明確聲明為不可見或被忽略,即 .gitignore 文件中聲明的文件(夾)。.gitignore 文件的格式如下:
- 空行會被忽略,而以井號(#)開頭的行可以用于注釋。然而,如果#跟在其他文本后面,它就不表示注釋了。
- 一個簡單的字面置文件名匹配任何目錄中的同名文件。
- 目錄名由末尾的反斜線(/)標(biāo)記。這能匹配同名的目錄和子目錄,但不匹配文件或符號鏈接。
- 包含shell通配符,如星號(*),這種模式可擴展為shell通配模式。正如標(biāo)準(zhǔn)shell通配符一樣,因為不能跨目錄匹配,所以一個星號只能匹配一個文件或目錄名。
- 起始的感嘆號(!)會對該行其余部分的模式進行取反。此外,被之前模式排除但被取反規(guī)則匹配的文件是要包含的。取反模式會覆蓋低優(yōu)先級的規(guī)則。
此外,Git允許在版本庫中任何目錄下有 .gitignore 文件,每個文件都只影響該目錄及其所有子目錄。.gitignore 的規(guī)則也是級聯(lián)的:可以覆蓋高層目錄中的規(guī)則。為了解決帶多個 .gitignore 目錄的層次結(jié)構(gòu)問題,也為了允許命令行對忽略文件列表的增編,Git按照下列從高到低的優(yōu)先順序?qū)ξ募M行忽略:- 在命令行上指定的模式( git update-index )
- 從與文件在相同目錄的 `.gitignore` 文件中讀取的模式
- 上層目錄中指定的模式(最接近當(dāng)前目錄的上層目錄的模式優(yōu)先于更上層的目錄的模式)
- 來自 `.git/info/exclude` 文件的模式
- 來自配置變量core.excludedfile指定的文件中的模式。
3. 未追蹤的(Untracked)
未追蹤的文件是指那些不在前兩類中的文件。Git把工作目錄下的所有文件當(dāng)成一個集合,減去已追蹤的文件和被忽略的文件,剩下的部分作為未追蹤的文件。在工作目錄下新建的文件也是未追蹤文件。
索引用來定位暫存區(qū)以及版本庫中的文件。當(dāng)對工作目錄下未追蹤文件或新的修改執(zhí)行 git add 時,這些內(nèi)容會被以blob對象的形式存入對象庫中,同時,index文件(二進制文件)記錄這些blob對象與文件的對應(yīng)關(guān)系。
實例展示:
初始時,倉庫中只有一個新建未提交的 file01.txt 文件:

對文件作出修改并將修改添加到暫存區(qū)后,index文件變更對應(yīng)關(guān)系:

HEAD、refs
HEAD文件記錄當(dāng)前工作目錄所對應(yīng)的git分支,refs文件夾記錄分支以及tag(tag指向某一次commit,用來給開發(fā)分支做一個標(biāo)記,以便后續(xù)回退)。
實例展示:
初始時,倉庫中有已提交的 file01.txt 以及 file02.txt 文件:

切換分支并提交新的commit后:

在分別給兩次commit打上tag后:

上述過程對應(yīng)圖示如下:

info
info目錄下只有exclude文件,其作用與 .gitignore 功能類似。他們的區(qū)別在于 .gitignore 這個文件本身也是存儲在對象庫中,用來保存的是公共需要排除的文件;而exclude文件中設(shè)置的則是本地需要排除的文件,不會影響到其他人。

description
description文件中的內(nèi)容用于GitWeb。GitWeb是Git提供的 CGI 腳本,讓用戶在web頁面查看git內(nèi)容。如果我們要啟動 GitWeb,可用命令 git instaweb --httpd=webrick 。這個命令在本地啟動了一個監(jiān)聽 1234 端口的 HTTP 服務(wù)器,并且自動打開了瀏覽器。想要關(guān)閉GitWeb,只需要在啟動命令的末尾加上 --stop 。打開的瀏覽器頁面如下:


上圖中description字段對應(yīng)的內(nèi)容就是 .git/description 文件中記錄的內(nèi)容。

hooks
hooks目錄下的文件如下:

Git 能在特定的動作發(fā)生時觸發(fā)自定義腳本,這些腳本(鉤子)都被存儲在 hooks子目錄中。如上圖,這些示例文件的名字都是以.sample結(jié)尾,如果想啟用這些鉤子,得先移除sample后綴。把一個正確命名(不帶擴展名)且可執(zhí)行的文件放入hooks子目錄中,即可激活該鉤子腳本。這樣一來,它就能被 Git 調(diào)用。
COMMIT_EDITMSG
COMMIT_EDITMSG文件記錄本地最后一次commit對應(yīng)的message。
logs
logs文件夾用來記錄操作信息,它的目錄如下:

HEAD文件記錄所有分支上的操作信息:

refs/heads/branch-name(分支名稱)文件記錄對應(yīng)分支上的操作信息:

