聊聊Git原理

提及Git,相信你們都很熟悉了,畢竟做爲程序猿,天天的業餘時間除了吃飯睡覺就是逛一下全世界最大的開(tong)源(xing)代(jiao)碼(you)網站GitHub了。在那裏Git是每一個人所要具有的最基本的技能。今天咱們不聊Git的基本應用,來聊一聊Git的原理。<!-- more -->git


Git給本身的定義是一套內存尋址文件系統,當你在一個目錄下執行git init命令時,會生成一個.git目錄,它的目錄結構是這樣的:bash

.git/
├── branches
├── config
├── description
├── HEAD
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── prepare-commit-msg.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   └── update.sample
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
   ├── heads
   └── tags


其中branches目錄已經再也不使用,description文件僅供GitWeb程序使用,config文件保存了項目的配置。微信


須要咱們重點關注的是HEAD和index文件以及objects和refs目錄。其中index中保存了暫存區的一些信息,這裏不作過多介紹。app


objects目錄

這個目錄是用來存儲Git對象的(包括tree對象、commit對象和blob對象),對於一個初始的Git倉庫,objects目錄下只有info和pack兩個子目錄,並無常規文件。隨着項目的進行,咱們建立的文件,以及一些操做記錄,都會做爲Git對象被存儲在這個目錄下。ide


在該目錄下,全部對象都會生成一個文件,而且有對應的SHA-1校驗和,Git會建立以校驗和前兩位爲名稱的子目錄,並以剩下的38位爲名稱來保存文件。post


接下來讓咱們一塊兒看一下當咱們進行一次提交時,Git具體作了哪些事情。學習

$ echo 'test content'>test.txt
$ git add .

執行上述命令後,objects目錄結構以下:網站

.git/objects/
├── d6
│   └── 70460b4b4aece5915caf5c68d12f560a9fe3e4
├── info
└── pack


這裏多了一個文件夾,如上面所述,這個就是Git爲咱們建立的一個對象,咱們可使用底層命令來看一下這個對象的類型以及它存儲的是什麼。ui

$ git cat-file -t d670460b4b4aece5915caf5c68d12f560a9fe3e4
blob
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

能夠看到,這是一個blob對象,存儲內容就是咱們剛剛建立的文件的內容。接下來繼續執行提交操做。spa

$ git commit -m 'test message'
[master (root-commit) 2b00dca] test message
1 file changed, 1 insertion(+)
create mode 100644 test.txt
$ tree .git/objects/
.git/objects/
├── 2b
│   └── 00dcae50af70bb5722033b3fe75281206c74da
├── 80
│   └── 865964295ae2f11d27383e5f9c0b58a8ef21da
├── d6
│   └── 70460b4b4aece5915caf5c68d12f560a9fe3e4
├── info
└── pack

此時objects目錄下又多了兩個對象。再用cat-file命令來查看一下這兩個文件。

$ git cat-file -t 2b00dcae50af70bb5722033b3fe75281206c74da
commit
$ git cat-file -p 2b00dcae50af70bb5722033b3fe75281206c74da
tree 80865964295ae2f11d27383e5f9c0b58a8ef21da
author jackeyzhe <jackeyzhe59@163.com> 1534670725 +0800
committer jackeyzhe <jackeyzhe59@163.com> 1534670725 +0800

test message
$ git cat-file -t 80865964295ae2f11d27383e5f9c0b58a8ef21da
tree
$ git cat-file -p 80865964295ae2f11d27383e5f9c0b58a8ef21da
100644 blob d670460b4b4aece5915caf5c68d12f560a9fe3e4    test.txt

能夠看到一個是commit對象,一個是tree對象。commit對象一般包括4部份內容:

  • 工做目錄快照的Hash,即tree的值

  • 提交的說明信息

  • 提交者的信息

  • 父提交的Hash值

因爲我是第一次提交,因此這裏沒有父提交的Hash值。


tree對象能夠理解爲UNIX文件系統中的目錄,保存了工做目錄的tree對象和blob對象的信息。接下來咱們再來看一下Git是如何進行版本控制的。

echo 'version1'>version.txt
$ git add .
$ git commit -m 'first version'
[master 702193d] first version
1 file changed, 1 insertion(+)
create mode 100644 version.txt
$ echo 'version2'>version.txt
$ git add .
$ git commit -m 'second version'
[master 5333a75] second version
1 file changed, 1 insertion(+), 1 deletion(-)
$ tree .git/objects/
.git/objects/
├── 1f
│   └── a5aab2a3cf025d06479b9eab9a7f66f60dbfc1
├── 29
│   └── 13bfa5cf9fb6f893bec60ac11d86129d56fcbe
├── 2b
│   └── 00dcae50af70bb5722033b3fe75281206c74da
├── 53
│   └── 33a759c4bdcdc6095b4caac19743d9445ca516
├── 5b
│   └── dcfc19f119febc749eef9a9551bc335cb965e2
├── 70
│   └── 2193d62ffd797155e4e21eede20897890da12a
├── 80
│   └── 865964295ae2f11d27383e5f9c0b58a8ef21da
├── d6
│   └── 70460b4b4aece5915caf5c68d12f560a9fe3e4
├── df
│   └── 7af2c382e49245443687973ceb711b2b74cb4a
├── info
└── pack
$ git cat-file -p 1fa5aab2a3cf025d06479b9eab9a7f66f60dbfc1
100644 blob d670460b4b4aece5915caf5c68d12f560a9fe3e4    test.txt
100644 blob 5bdcfc19f119febc749eef9a9551bc335cb965e2    version.txt
$ git cat-file -p 2913bfa5cf9fb6f893bec60ac11d86129d56fcbe
100644 blob d670460b4b4aece5915caf5c68d12f560a9fe3e4    test.txt
100644 blob df7af2c382e49245443687973ceb711b2b74cb4a    version.txt

Git將沒有改變的文件的Hash值直接存入tree對象,對於有修改的文件,則會生成一個新的對象,將新的對象存入tree對象。咱們再來看一下commit對象的信息。

$ git cat-file -p 5333a759c4bdcdc6095b4caac19743d9445ca516
tree 2913bfa5cf9fb6f893bec60ac11d86129d56fcbe
parent 702193d62ffd797155e4e21eede20897890da12a
author jackeyzhe <jackeyzhe59@163.com> 1534672270 +0800
committer jackeyzhe <jackeyzhe59@163.com> 1534672270 +0800

second version
$ git cat-file -p 702193d62ffd797155e4e21eede20897890da12a
tree 1fa5aab2a3cf025d06479b9eab9a7f66f60dbfc1
parent 2b00dcae50af70bb5722033b3fe75281206c74da
author jackeyzhe <jackeyzhe59@163.com> 1534672248 +0800
committer jackeyzhe <jackeyzhe59@163.com> 1534672248 +0800

first version

此時的commit對象已經有parent信息了,這樣咱們就能夠順着parent一步步往回進行版本回退了。不過這樣是比較麻煩的,咱們通常習慣用的是git log查看提交記錄。


refs目錄

在介紹refs目錄以前,咱們仍是先來看一下該目錄結構

$ tree .git/refs/
.git/refs/
├── heads
│   └── master
└── tags

2 directories, 1 file
$ cat .git/refs/heads/master
5333a759c4bdcdc6095b4caac19743d9445ca516

在一個剛剛被初始化的Git倉庫中,refs目錄下只有heads和tags兩個子目錄,因爲咱們剛剛有過提交操做,因此git爲咱們自動生成了一個名爲master的引用。master的內容是最後一次提交對象的Hash值。看到這裏你們必定在想,若是咱們對每次提交都建立一個這樣的引用,不就不須要記住每次提交的Hash值了,只要看看引用的值,複製過來就能夠退回到對應版本了。沒錯,這樣是能夠方便的退回,可是這樣作的意義不大,由於咱們並不須要頻繁的退回,特別是比較古老的版本,退回的機率更是趨近於0。Git用這個引用作了更有意義的事,那就是分支。


當我新建一個分支時,git就會在.git/refs/heads目錄下新建一個文件。固然新建的引用仍是指向當前工做目錄的最後一次提交,通常狀況下咱們不會主動去修改這些引用文件,不過若是必定要修改,Git爲咱們提供了一個update-ref命令。能夠改變引用的值,使其指向不一樣的commit對象。


tags目錄下的文件存儲的是標籤對應的commit,當爲某次提交打上一個tag時,tags目錄下就會被建立出一個命名爲tag名的文件,值是這次提交的Hash值。


HEAD

新建分支的時候,Git是怎麼知道咱們當前是在哪一個分支的,Git又是如何實現分支切換的呢?答案就在HEAD這個文件中。

$ cat .git/HEAD 
ref: refs/heads/master
$ git checkout test
Switched to branch 'test'
$ cat .git/HEAD
ref: refs/heads/test

很明顯,HEAD文件存儲的就是咱們當前分支的引用,當咱們切換分支後再次進行提交操做時,Git就會讀取HEAD對應引用的值,做爲這次commit的parent。咱們也能夠經過symbolic-ref命令手動設置HEAD的值,可是不能設置refs之外的形式。


Packfiles

到這裏咱們在文章開頭所說的重點關注的目錄和文件都介紹完畢了。可是做爲一個文件系統,還存在一個問題,那就是空間。前文介紹過,當文件修改後進行提交時,Git會建立一份新的快照。這樣長久下去,一定會佔用很大的存儲空間。而比較古老的版本的價值已經不大,因此要想辦法清理出足夠的空間供用戶使用。


好消息是,Git擁有本身的gc(垃圾回收)方法。當倉庫中有太多鬆散對象時,Git會調用git gc命令(固然咱們也能夠手動調用這個命令),將這些對象進行打包。打包後會出現兩個新文件:一個idx索引文件和一個pack文件。索引文件包含了packfile的偏移信息,能夠快速定位到文件。打包後,每一個文件最新的版本的對象存的是完整的文件內容。而以前的版本只保存差別。這樣就達到了壓縮空間的目的。


Ending

本文只介紹了Git的原理,若是對Git基本操做不熟悉的話,能夠點擊閱讀原文學習Pro Git


本文分享自微信公衆號 - 代碼潔癖患者(Jackeyzhe2018)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索