一、Git 分支本質
如果對倉庫中從一個提交(比如 1a410e)開始往前的歷史感興趣,那么可以運行 git log 1a410e 這樣的命令來顯示歷史,不過需要記得 1a410e 是查看歷史的起點提交。如果我們有一個文件來保存 SHA-1 值,而該文件有一個簡單的名字, 然后用這個名字指針來替代原始的 SHA-1 值的話會更加簡單。 在 Git 中,這種簡單的名字被稱為“引用(references,或簡寫為 refs)”,可以在 .git/refs 目錄下找到這類含有 SHA-1 值的文件。在目前的項目中,這個目錄沒有包含任何文件,但它包含了一個簡單的目錄結構:
$ find
. git
/ refs
. git
/ refs
. git
/ refs
/ heads
. git
/ refs
/ tags
$ find
. git
/ refs
- type f
若要創建一個新引用來幫助記憶最新提交所在的位置,從技術上講只需簡單地做如下操作:
$ echo
1 a410efbd13591db07496601ebc7a059dd55cfe9
> . git
/ refs
/ heads
/ master
現在,就可以在 Git 命令中使用這個剛創建的新引用來代替 SHA-1 值:
$ git log
-- pretty
= oneline master
1 a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
但是,不提倡直接編輯引用文件,如果想更新某個引用,Git 提供了一個更加安全的命令 update-ref 來完成此事:
$ git update
- ref refs
/ heads
/ master
1 a410efbd13591db07496601ebc7a059dd55cfe9
這基本就是 Git 分支的本質:一個指向某一系列提交之首的指針或引用,若想在第二個提交上創建一個分支,可以這么做:
$ git update
- ref refs
/ heads
/ test cac0ca
$ git log
-- pretty
= oneline test
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
至此,我們的 Git 數據庫從概念上看起來像這樣:
當運行類似于 git branch 這樣的命令時,Git 實際上會運行 update-ref 命令,取得當前所在分支最新提交對應的 SHA-1 值,并將其加入想要創建的任何新引用中。
二、HEAD 引用
現在的問題是,當執行 git branch 時,Git 如何知道最新提交的 SHA-1 值呢? 答案是 HEAD 文件。 HEAD 文件通常是一個符號引用(symbolic reference),指向目前所在的分支。所謂符號引用,表示它是一個指向其他引用的指針。 然而在某些罕見的情況下,HEAD 文件可能會包含一個 git 對象的 SHA-1 值。當在檢出一個標簽、提交或遠程分支,讓倉庫變成 “分離 HEAD”狀態時,就會出現這種情況。 如果查看 HEAD 文件的內容,通常會看到類似這樣的內容:
$ cat
. git
/ HEAD
ref
: refs
/ heads
/ master
如果執行 git checkout test,Git 會像這樣更新 HEAD 文件:
$ cat
. git
/ HEAD
ref
: refs
/ heads
/ test
當執行 git commit 時,該命令會創建一個提交對象,并用 HEAD 文件中那個引用所指向的 SHA-1 值設置其父提交字段。 也可以手動編輯該文件,然而同樣存在一個更安全的命令來完成此事:git symbolic-ref,借助此命令來查看 HEAD 引用對應的值:
$ git symbolic
- ref HEAD
refs
/ heads
/ master
$ git symbolic
- ref HEAD refs
/ heads
/ test
$ cat
. git
/ HEAD
ref
: refs
/ heads
/ test
$ git symbolic
- ref HEAD test
fatal
: Refusing to point HEAD outside of refs
/
三、標簽引用
我們知道, Git 有三種主要的對象類型(數據對象、樹對象和提交對象,具體請參考:Git內部原理之深入解析Git對象),然而實際上還有第四種:標簽對象(tag object), 它非常類似于一個提交對象,包含一個標簽創建者信息、一個日期、一段注釋信息,以及一個指針。主要的區別在于,標簽對象通常指向一個提交對象,而不是一個樹對象,像是一個永不移動的分支引用,永遠指向同一個提交對象,只不過給這個提交對象加上一個更友好的名字罷了。 正如 Git之深入解析本地倉庫的基本操作·倉庫的獲取更新和提交歷史的查看撤銷以及標簽別名的使用 中所討論的那樣,存在兩種類型的標簽:附注標簽和輕量標簽,可以像這樣創建一個輕量標簽:
$ git update
- ref refs
/ tags
/ v1
. 0 cac0cab538b970a37ea1e769cbbde608743bc96d
這就是輕量標簽的全部內容,一個固定的引用。 然而,一個附注標簽則更復雜一些,若要創建一個附注標簽,Git 會創建一個標簽對象,并記錄一個引用來指向該標簽對象,而不是直接指向提交對象。可以通過創建一個附注標簽來驗證這個過程(使用 -a 選項):
$ git tag
- a v1
. 1 1 a410efbd13591db07496601ebc7a059dd55cfe9
- m
'test tag'
$ cat
. git
/ refs
/ tags
/ v1
. 1
9585191f 37f 7 b0fb9444f35a9bf50de191beadc2
現在對該 SHA-1 值運行 git cat-file -p 命令:
$ git cat
- file
- p
9585191f 37f 7 b0fb9444f35a9bf50de191beadc2
object
1 a410efbd13591db07496601ebc7a059dd55cfe9
type commit
tag v1
. 1
tagger Scott Chacon
< schacon@gmail
. com
> Sat May
23 16 : 48 : 58 2009 - 0700 test tag
不難注意到,object 條目指向打了標簽的那個提交對象的 SHA-1 值。另外,標簽對象并非必須指向某個提交對象,可以對任意類型的 Git 對象打標簽。例如,在 Git 源碼中,項目維護者將它們的 GPG 公鑰添加為一個數據對象,然后對這個對象打了一個標簽,可以克隆一個 Git 版本庫,然后通過執行下面的命令來在這個版本庫中查看上述公鑰:
$ git cat
- file blob junio
- gpg
- pub
Linux 內核版本庫同樣有一個不指向提交對象的標簽對象,首個被創建的標簽對象所指向的是最初被引入版本庫的那份內核源碼所對應的樹對象。
四、遠程引用
現在將看到的第三種引用類型是遠程引用(remote reference),如果添加了一個遠程版本庫并對其執行過推送操作,Git 會記錄下最近一次推送操作時每一個分支所對應的值,并保存在 refs/remotes 目錄下。例如,可以添加一個叫做 origin 的遠程版本庫,然后把 master 分支推送上去:
$ git remote add origin git@github
. com
: schacon
/ simplegit
- progit
. git
$ git push origin master
Counting objects
: 11 , done
.
Compressing objects
: 100 % ( 5 / 5 ) , done
.
Writing objects
: 100 % ( 7 / 7 ) , 716 bytes
, done
.
Total
7 ( delta
2 ) , reused
4 ( delta
1 )
To git@github
. com
: schacon
/ simplegit
- progit
. gita11bef0
. . ca82a6d master
-> master
此時,如果查看 refs/remotes/origin/master 文件,可以發現 origin 遠程版本庫的 master 分支所對應的 SHA-1 值,就是最近一次與服務器通信時本地 master 分支所對應的 SHA-1 值:
$ cat
. git
/ refs
/ remotes
/ origin
/ master
ca82a6dff817ec66f44342007202690a93763949
遠程引用和分支(位于 refs/heads 目錄下的引用)之間最主要的區別在于,遠程引用是只讀的。雖然可以 git checkout 到某個遠程引用,但是 Git 并不會將 HEAD 引用指向該遠程引用。因此,永遠不能通過 commit 命令來更新遠程引用,Git 將這些遠程引用作為記錄遠程服務器上各分支最后已知位置狀態的書簽來管理。
五、包文件
如果跟著做完了上文的所有步驟,那么現在應該有了一個測試用的 Git 倉庫, 其中包含 11 個對象:四個數據對象,三個樹對象,三個提交對象和一個標簽對象:
$ find
. git
/ objects
- type f
. git
/ objects
/ 01 / 55 eb4229851634a0f03eb265b69f5a2d56f341 # tree
2
. git
/ objects
/ 1 a
/ 410 efbd13591db07496601ebc7a059dd55cfe9 # commit
3
. git
/ objects
/ 1f / 7 a7a472abf3dd9643fd615f6da379c4acb3e3a # test
. txt v2
. git
/ objects
/ 3 c
/ 4e9 cd789d88d8d89c1073707c3585e41b0e614 # tree
3
. git
/ objects
/ 83 / baae61804e65cc73a7201a7252750c76066a30 # test
. txt v1
. git
/ objects
/ 95 / 85191f 37f 7 b0fb9444f35a9bf50de191beadc2 # tag
. git
/ objects
/ ca
/ c0cab538b970a37ea1e769cbbde608743bc96d # commit
2
. git
/ objects
/ d6
/ 70460 b4b4aece5915caf5c68d12f560a9fe3e4 #
'test content'
. git
/ objects
/ d8
/ 329f c1cc938780ffdd9f94e0d364e0ea74f579 # tree
1
. git
/ objects
/ fa
/ 49 b077972391ad58037050f2a75f74e3671e92 # new
. txt
. git
/ objects
/ fd
/ f4fc3344e67ab068f836878b6c4951e3b15f3d # commit
1
Git 使用 zlib 壓縮這些文件的內容,而且并沒有存儲太多東西,所以上文中的文件一共只占用了 925 字節。接下來,我們添加一些大文件到倉庫中,以此展示 Git 的一個很有趣的功能。為了便于演示,要把之前在 Grit 庫中用到過的 repo.rb 文件添加進來,如下所示,這是一個大小約為 22K 的源代碼文件:
$ curl https
:
$ git checkout master
$ git add repo
. rb
$ git commit
- m
'added repo.rb'
[ master
484 a592
] added repo
. rb
3 files changed
, 709 insertions ( + ) , 2 deletions ( - ) delete mode
100644 bak
/ test
. txtcreate mode
100644 repo
. rbrewrite test
. txt ( 100 % )
如果查看生成的樹對象,可以看到 repo.rb 文件對應的數據對象的 SHA-1 值:
$ git cat
- file
- p master
^ { tree
}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new
. txt
100644 blob
033 b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 repo
. rb
100644 blob e3f094f522629ae358806b17daf78246c27c007b test
. txt
接下來可以使用 git cat-file 命令查看這個對象有多大:
$ git cat
- file
- s
033 b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5
22044
$ echo
'# testing' >> repo
. rb
$ git commit
- am
'modified repo.rb a bit'
[ master
2431 da6
] modified repo
. rb a bit
1 file changed
, 1 insertion ( + )
查看這個最新的提交生成的樹對象,可以看到一些有趣的東西:
$ git cat
- file
- p master
^ { tree
}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new
. txt
100644 blob b042a60ef7dff760008df33cee372b945b6e884e repo
. rb
100644 blob e3f094f522629ae358806b17daf78246c27c007b test
. txt
repo.rb 對應一個與之前完全不同的數據對象,這意味著,雖然只是在一個 400 行的文件后面加入一行新內容,Git 也會用一個全新的對象來存儲新的文件內容:
$ git cat
- file
- s b042a60ef7dff760008df33cee372b945b6e884e
22054
磁盤上現在有兩個幾乎完全相同、大小均為 22K 的對象(每個都被壓縮到大約 7K),如果 Git 只完整保存其中一個,再保存另一個對象與之前版本的差異內容,豈不更好? 事實上 Git 可以那樣做,最初向磁盤中存儲對象時所使用的格式被稱為“松散(loose)”對象格式。但是,Git 會時不時地將多個這些對象打包成一個稱為“包文件(packfile)”的二進制文件,以節省空間和提高效率,當版本庫中有太多的松散對象,或者手動執行 git gc 命令,或者向遠程服務器執行推送時,Git 都會這樣做。要看到打包過程,可以手動執行 git gc 命令讓 Git 對對象進行打包:
$ git gc
Counting objects
: 18 , done
.
Delta compression using up to
8 threads
.
Compressing objects
: 100 % ( 14 / 14 ) , done
.
Writing objects
: 100 % ( 18 / 18 ) , done
.
Total
18 ( delta
3 ) , reused
0 ( delta
0 )
這個時候再查看 objects 目錄,會發現大部分的對象都不見了,與此同時出現了一對新文件:
$ find
. git
/ objects
- type f
. git
/ objects
/ bd
/ 9 dbf5aae1a3862dd1526723246b20206e5fc37
. git
/ objects
/ d6
/ 70460 b4b4aece5915caf5c68d12f560a9fe3e4
. git
/ objects
/ info
/ packs
. git
/ objects
/ pack
/ pack
- 978e03944f 5 c581011e6998cd0e9e30000905586
. idx
. git
/ objects
/ pack
/ pack
- 978e03944f 5 c581011e6998cd0e9e30000905586
. pack
仍保留著的幾個對象是未被任何提交記錄引用的數據對象,在此例中是之前創建的 “what is up, doc?” 和 “test content” 這兩個示例數據對象,因為從沒將它們添加至任何提交記錄中,所以 Git 認為它們是懸空(dangling)的,不會將它們打包進新生成的包文件中。 剩下的文件是新創建的包文件和一個索引,包文件包含了剛才從文件系統中移除的所有對象的內容,索引文件包含了包文件的偏移信息,我們通過索引文件就可以快速定位任意一個指定對象。有意思的是運行 gc 命令前磁盤上的對象大小約為 15K,而這個新生成的包文件大小僅有 7K,通過打包對象減少了一半的磁盤占用空間。 Git 是如何做到這點的呢? Git 打包對象時,會查找命名及大小相近的文件,并只保存文件不同版本之間的差異內容。可以查看包文件,觀察它是如何節省空間的,git verify-pack 這個底層命令可以就可以查看已打包的內容:
$ git verify
- pack
- v
. git
/ objects
/ pack
/ pack
- 978e03944f 5 c581011e6998cd0e9e30000905586
. idx
2431 da676938450a4d72e260db3bf7b0f587bbc1 commit
223 155 12
69 bcdaff5328278ab1c0812ce0e07fa7d26a96d7 commit
214 152 167
80 d02664cb23ed55b226516648c7ad5d0a3deb90 commit
214 145 319
43168 a18b7613d1281e5560855a83eb8fde3d687 commit
213 146 464
092917823486 a802e94d727c820a9024e14a1fc2 commit
214 146 610
702470739 ce72005e2edff522fde85d52a65df9b commit
165 118 756
d368d0ac0678cbe6cce505be58126d3526706e54 tag
130 122 874
fe879577cb8cffcdf25441725141e310dd7d239b tree
136 136 996
d8329fc1cc938780ffdd9f94e0d364e0ea74f579 tree
36 46 1132
deef2e1b793907545e50a2ea2ddb5ba6c58c4506 tree
136 136 1178
d982c7cb2c2a972ee391a85da481fc1f9127a01d tree
6 17 1314 1 \deef2e1b793907545e50a2ea2ddb5ba6c58c4506
3 c4e9cd789d88d8d89c1073707c3585e41b0e614 tree
8 19 1331 1 \deef2e1b793907545e50a2ea2ddb5ba6c58c4506
0155 eb4229851634a0f03eb265b69f5a2d56f341 tree
71 76 1350
83 baae61804e65cc73a7201a7252750c76066a30 blob
10 19 1426
fa49b077972391ad58037050f2a75f74e3671e92 blob
9 18 1445
b042a60ef7dff760008df33cee372b945b6e884e blob
22054 5799 1463
033 b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob
9 20 7262 1 \b042a60ef7dff760008df33cee372b945b6e884e
1f 7 a7a472abf3dd9643fd615f6da379c4acb3e3a blob
10 19 7282
non delta
: 15 objects
chain length
= 1 : 3 objects
. git
/ objects
/ pack
/ pack
- 978e03944f 5 c581011e6998cd0e9e30000905586
. pack
: ok
此處,033b4 這個數據對象(即 repo.rb 文件的第一個版本)引用了數據對象 b042a,即該文件的第二個版本,命令輸出內容的第三列顯示的是各個對象在包文件中的大小,可以看到 b042a 占用了 22K 空間,而 033b4 僅占用 9 字節。同樣有趣的地方在于,第二個版本完整保存了文件內容,而原始的版本反而是以差異方式保存的,這是因為大部分情況下需要快速訪問文件的最新版本。 最妙之處是可以隨時重新打包,Git 時常會自動對倉庫進行重新打包以節省空間。當然也可以隨時手動執行 git gc 命令來這么做。
總結
以上是生活随笔 為你收集整理的Git内部原理之深入解析Git的引用和包文件 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。