Git是怎麼工作的 Git與其它版本控制系統的差異 object(對象) 引用 遠程分支 pack機制

相信大部分的程序員都會使用Git,可能使用IDE集成的可視化界面,也可能是直接用的命令行。但是可能很多人對Git的一些原理性的概念並沒有什麼瞭解,這篇博客就從Git的原理出發,講解Git的一些命令操作的底層意義,可能會讓大家使用Git的時候更加得心應手。

PS: 這篇博客是對部門技術分享的整理,絕大多數知識點可以在Pro Git這本書裏面找到,也有部分篇幅摘抄自這本書,我也強推大家去通讀一遍這本書。

Git與其它版本控制系統的差異

Git 和其它版本控制系統(包括 Subversion 和近似工具)的主要差別在於 Git 對待數據的方法。 概念上來區分,其它大部分系統以文件變更列表的方式存儲信息。 這類系統(CVS、Subversion、Perforce、Bazaar 等等)將它們保存的信息看作是一組基本文件和每個文件隨時間逐步累積的差異。

Git 不按照以上方式對待或保存數據。 反之,Git 更像是把數據看作是對小型文件系統的一組快照。 每次你提交更新,或在 Git 中保存項目狀態時,它主要對當時的全部文件製作一個快照並保存這個快照的索引。 爲了高效,如果文件沒有修改,Git 不再重新存儲該文件,而是隻保留一個鏈接指向之前存儲的文件。 Git 對待數據更像是一個 快照流

也就是說Git實際是實現了一個小型的文件系統,把需要託管的文件的所有版本都備份到了這個系統裏面,然後在需要的時候找到對應的版本拿出來使用。

大家都知道Git是Linus開發的,寫操作系統內核的人思路的確就是和其他人的不一樣,直接搞了個文件系統,而且還繼承了Linux一切皆文件的思想。

object(對象)

這個文件系統其實就在.git/objects目錄裏面,git把所有的提交文件、提交歷史等都保存成一個object保存到這個目錄裏面。

當clone了一個空的項目的時候.git/objects目錄裏面會有幾個子目錄,但是並沒有實際的文件:

tree .git/objects
.git/objects
├── info
└── pack

當我們做了添加了一些目錄和文件:

tree .
.
├── dir_a
│   └── file_1
└── dir_b
    └── file_2

然後將它們提交到Git之後,Git就會對這些目錄和文件原內容加上特定頭部信息一起做SHA-1散列得到一個校驗和,並且將它們保存到objects目錄。散列值前兩字符用於命名子目錄,餘下的38個字符則用作文件名,這個文件就是Git的存儲對象(obejct):

tree .git/objects
.git/objects
├── 26
│   └── f76fe87ba045f5a6b40d93598ca96e5a1fab39
├── 31
│   └── 2f01ad223d5eb1e959122ac2829744b59fd7f2
├── 75
│   └── 8902e9f92ad80673cb0f1da4b8d34fbfe47544
├── 8a
│   └── 776b73f34c3b546a66fe6173dfa0a53a142b9b
├── b8
│   └── 947b77094228836f18792dc5fac15dfa9de11e
├── c5
│   └── 0a174948973a2dbe8a43fd9282d24a7a6074c4
├── info
└── pack

這些文件都是壓縮過的,我們可以用git cat-file名去查看內容,由於這些文件都是有不同類型區分的,所以可以用-p參數自動識別文件類型,例如我們查看26f76fe87ba045f5a6b40d93598ca96e5a1fab39這個object:

git cat-file -p 26f76fe87ba045f5a6b40d93598ca96e5a1fab39
040000 tree 758902e9f92ad80673cb0f1da4b8d34fbfe47544    dir_a
040000 tree 8a776b73f34c3b546a66fe6173dfa0a53a142b9b    dir_b

它是就是我們的根目錄,可以看到有兩個tree類型的object,分別對應子目錄dir_a和dir_b,我們進去dir_a看看:

git cat-file -p 758902e9f92ad80673cb0f1da4b8d34fbfe47544
100644 blob c50a174948973a2dbe8a43fd9282d24a7a6074c4    file_1

這裏有個blob類型的object file_1,blob類型的object就用來文件快照,可以看到它保存了file_1的所有內容:

git cat-file -p c50a174948973a2dbe8a43fd9282d24a7a6074c4
file 1 content

除了這些目錄、文件的object之外我們的commit也是會被保存成一個obejct:

git cat-file -p 312f01ad223d5eb1e959122ac2829744b59fd7f2
tree 26f76fe87ba045f5a6b40d93598ca96e5a1fab39
author linjw <[email protected]> 1597981085 +0800
committer linjw <[email protected]> 1597981085 +0800

first commit

可以看到它有個tree字段指向了根目錄的object(26f76fe87ba045f5a6b40d93598ca96e5a1fab39)

這個commit的object的校驗和其實就是commit id:

commit 312f01ad223d5eb1e959122ac2829744b59fd7f2 (HEAD -> master)
Author: linjw <[email protected]>
Date:   Fri Aug 21 11:38:05 2020 +0800

    first commit

所以通過這個commit我們就能構建起整個目錄:

對象樹

然後我們再修改下file_1,提交個commit,commit id 是da64cc3756675914aab6df4c01b81539ae6ef39f,我們查看它的內容,發現它對比第一個commit多了個parent指向第一個commit:

git cat-file -p da64cc3756675914aab6df4c01b81539ae6ef39f
tree 8cbab3b672a04bc8bf5fa63af6e06b0d92bd4126
parent 312f01ad223d5eb1e959122ac2829744b59fd7f2
author linjw <[email protected]> 1597982034 +0800
committer linjw <[email protected]> 1597982034 +0800

modifying file_1

然後現在整個objects目錄是這樣的:

tree .git/objects
.git/objects
├── 1e
│   └── a40765c24dc3a9109c06d2e2c1408ea40568be
├── 26
│   └── f76fe87ba045f5a6b40d93598ca96e5a1fab39
├── 31
│   └── 2f01ad223d5eb1e959122ac2829744b59fd7f2
├── 70
│   └── d733623d440ad95d53272528ae900295855665
├── 75
│   └── 8902e9f92ad80673cb0f1da4b8d34fbfe47544
├── 8a
│   └── 776b73f34c3b546a66fe6173dfa0a53a142b9b
├── 8c
│   └── bab3b672a04bc8bf5fa63af6e06b0d92bd4126
├── b8
│   └── 947b77094228836f18792dc5fac15dfa9de11e
├── c5
│   └── 0a174948973a2dbe8a43fd9282d24a7a6074c4
├── da
│   └── 64cc3756675914aab6df4c01b81539ae6ef39f
├── info
└── pack

我們用下面這張圖表示各個object 的關係:

這些object是以樹形結構組織起來的,而且每個commit都能遍歷找到那個版本的所有文件,所以當使用reset命令的時候只需要找到commit的object然後遍歷對象樹將object裏面的內容解壓出來替換工作區的文件就可以了。

引用

在理解了commit本質上其實是一個object之後,我們就能很容易理解引用這個概念了。

引用在Git裏面其實本質也是一個文件,它們存放在.git/refs/下的子目錄裏面,例如本地引用的路徑在.git/refs/heads目錄裏面:

tree .git/refs/heads
.git/refs/heads
└── master

我們可以看到現在裏面只有個master文件,原因是我們只有一個master分支。讓我們打印一下這個master文件的內容:

cat .git/refs/heads/master
da64cc3756675914aab6df4c01b81539ae6ef39f

它的內容其實就是object的檢驗和。讓我們創建多一個develop分支,可以看到這個目錄就多了個develop文件

tree .git/refs/heads
.git/refs/heads
├── develop
└── master

它的內容和master的一樣:

cat .git/refs/heads/develop
da64cc3756675914aab6df4c01b81539ae6ef39f

讓我們回滾這個分支到第一個提交,可以發現它的內容就變成了第一個commit的校驗和:

cat .git/refs/heads/develop
312f01ad223d5eb1e959122ac2829744b59fd7f2

其實除了分支之外,我們打的tag也是一樣的原理,例如我們此時在develop上一個v1.0的tag,就會發現.git/refs/tags/目錄下多了一個v1.0的文件:

tree .git/refs/tags
.git/refs/tags
└── v1.0

它的內容也是第一個commit:

cat .git/refs/tags/v1.0
312f01ad223d5eb1e959122ac2829744b59fd7f2

現在引用的情況如下圖:

所以我們的分支也好,tag也好,其實都是一個引用,它們本質上是一個文件,裏面的內容就是指向的object的校驗和,而我們的回滾代碼,其實就是將各個分支的引用指向了不同的commit而已,如果我們在master分支將代碼reset到commit 312f01,就會發現master的引用指向了這個commit。

HEAD引用

那Git又是如何知道我們當前是在哪個分支的呢?

其實在Git裏面還有個特殊的引用HEAD引用,它就在.git目錄下面。我們可以打印下它的內容:

cat HEAD
ref: refs/heads/develop

可以發現它指向了我們的develop引用, 這就表示我們當前正在develop分支。

Git就是靠這個HEAD引用找到我們當前位於哪個commit:

當然HEAD的內容也可能直接指向某個commit號,例如我們checkout到某個tag的時候:

cat .git/HEAD
312f01ad223d5eb1e959122ac2829744b59fd7f2

這是因爲tag是固定的,我們並不能直接修改tag指向的commit。

reflog

我們都知道可以用git log去查看commit的日誌,其實類似的我們可以用git reflog去查看引用的操作日誌,它會的打印如下:

3cb383a (HEAD -> develop) HEAD@{0}: commit: difying file_2
312f01a (tag: v1.0) HEAD@{1}: checkout: moving from 312f01ad223d5eb1e959122ac2829744b59fd7f2 to develop
312f01a (tag: v1.0) HEAD@{2}: checkout: moving from develop to v1.0
312f01a (tag: v1.0) HEAD@{3}: reset: moving to 312f01ad223d5eb1e959122ac2829744b59fd7f2
da64cc3 (master) HEAD@{4}: checkout: moving from master to develop
da64cc3 (master) HEAD@{5}: commit: modifying file_1
312f01a (tag: v1.0) HEAD@{6}: commit (initial): first commit

這個東西有什麼用呢?舉個例子,假設我現在在develop修改了file_2提交了一個commit,沒有推到服務器上,然後就reset --hard回到了上一個commit。這個時候突然反悔了想找到之前那個commit要怎麼辦?

對的,就是用reflog:

312f01a (HEAD -> develop, tag: v1.0) HEAD@{0}: reset: moving to 312f01ad223d5eb1e959122ac2829744b59fd7f2
3cb383a HEAD@{1}: commit: difying file_2
312f01a (HEAD -> develop, tag: v1.0) HEAD@{2}: checkout: moving from 312f01ad223d5eb1e959122ac2829744b59fd7f2 to develop
312f01a (HEAD -> develop, tag: v1.0) HEAD@{3}: checkout: moving from develop to v1.0
312f01a (HEAD -> develop, tag: v1.0) HEAD@{4}: reset: moving to 312f01ad223d5eb1e959122ac2829744b59fd7f2
da64cc3 (master) HEAD@{5}: checkout: moving from master to develop
da64cc3 (master) HEAD@{6}: commit: modifying file_1
312f01a (HEAD -> develop, tag: v1.0) HEAD@{7}: commit (initial): first commit

從下往上,可以看到HEAD引用的操作歷史:

提交了第一個commit(312f01a)

---> 提交了第二個commit(da64cc3)

---> 從master切換到了develop分支,當前所處的commit號依然是da64cc3

---> 移動回了commit 312f01a

---> 從develop分支切換 到了v1.0這個tag,當前所處的commit號依然是312f01a

---> 切換回了develop分支,當前所處的commit號依然是312f01a

---> 修改了file_2提交了commit 3cb383a

----> reset 回到了commit 312f01a

所以我們就能找到丟失了的commit 3cb383a,此時只需要用reset --hard 3cb383a就能回到那個commit了。

遠程分支

遠程引用是對遠程倉庫的引用。我們從服務器拉取代碼的時候就會將服務器的分支引用拉到本地,它們的文件在.git/refs/remotes/目錄下的遠程倉庫對應的子目錄裏。例如我們在git clone的時候,Git會默認幫我們將遠程倉庫命名爲origin,所以它的分支引用文件就在.git/refs/remotes/origin/目錄下面。

這些遠程分支以<remote>/<branch>的形式命名,例如origin倉庫的master分支的名字就叫origin/master,所以我們可以用checkout命令直接切到遠程分支:

git checkout origin/master

當有其他人往服務器推代碼之後,我們需要用git fetch命令來抓取遠程倉庫有,而本地沒有的數據:

git fetch origin

抓取完之後遠程分支就更新了:

這個時候就可以用git merge命令將遠程分支的代碼合併到本地:

git merge origin/master

而我們工作中常用的git pull 在大多數情況下它的含義是一個 git fetch 緊接着一個 git merge 命令

順便一講,之前我們講到commit是有parent概念的,而第一個commit由於之前已經沒有提交了,所以它沒有parent,普通的commit會有一個parent。

git merge命令由於需要合併兩個分支的修改,所以它會生成一個新的commit,它有兩個parent:

例如我們上面的C6這commit就有兩個parent C4和從C5

pack機制

從上面我們可以看到Git向磁盤中存儲對象使用鬆散對象格式,一個文件、目錄、commit等對應一個文件,這樣的操作可能會比較簡單,但是其實是比較浪費磁盤空間的。而且在需要推送到遠程倉庫的時候需要一個個文件上傳效率也比較低。所以Git會時不時將這些文件打包在一起以節省空間提高網絡傳輸效率。

我們可以收到調用git gc命令讓Git進行打包並清理一些不需要的對象。打包完成之後.git/objects裏面的文件就會變小,並且在.git/objects/pack下面多出打包文件:

tree .git/objects/pack
.git/objects/pack
├── pack-20f597c6ab0c05f5c907023edf4e282be00ad6fe.idx
└── pack-20f597c6ab0c05f5c907023edf4e282be00ad6fe.pack

.idx文件是索引文件,而.pack就是將object對象打包成的二進制包,我們可以用git verify-pack -v命令查看包裏的信息:

git verify-pack -v .git/objects/pack/pack-20f597c6ab0c05f5c907023edf4e282be00ad6fe.idx
1dcac3d26ede1fe64eddd3511a74d99a96ef97e2 commit 202 144 12
3fbc341a1b669518f3ef7aa038d71f2f7d68a5f0 commit 207 150 156
34ffe485561f71337d0b64fca8dc9c8d57d86027 commit 78 88 306 1 3fbc341a1b669518f3ef7aa038d71f2f7d68a5f0
58d65a51d2e0288b577c73a660f467bd548033f8 commit 231 166 394
ace7ce09e72571eb0474221f8e52b1767cebb0db commit 269 190 560
feafba560a569ff272319bb98dba0869aa2b242c commit 48 59 750 1 ace7ce09e72571eb0474221f8e52b1767cebb0db
9aaa265d2221fd96e5d3dd420fe9d93e7b99726f commit 13 24 809 1 58d65a51d2e0288b577c73a660f467bd548033f8
2cb88bd69545620211e77a7865b9e9990d9a0c20 commit 212 150 833
9750080a17990f284f5045e4eef885605f2eb6d8 commit 80 91 983 1 2cb88bd69545620211e77a7865b9e9990d9a0c20
69f9a2ec2f55ceed476a68190d32f543d89c2f78 commit 70 82 1074 1 3fbc341a1b669518f3ef7aa038d71f2f7d68a5f0
da5e1a12ab4c5fdca2a808e4b85c1c54dc780642 commit 81 91 1156 1 3fbc341a1b669518f3ef7aa038d71f2f7d68a5f0
c756597a8071fbf0b26fda95f0cd99edb68f7759 commit 79 90 1247 1 2cb88bd69545620211e77a7865b9e9990d9a0c20
3cb383a390cbd7cbf4872412b828987e0cdc1b13 commit 213 153 1337
da64cc3756675914aab6df4c01b81539ae6ef39f commit 64 76 1490 1 58d65a51d2e0288b577c73a660f467bd548033f8
312f01ad223d5eb1e959122ac2829744b59fd7f2 commit 163 119 1566
e4e3854198486f045f95c16d886a7ecec076799d tree   64 67 1685
758902e9f92ad80673cb0f1da4b8d34fbfe47544 tree   34 45 1752
593e3181013a603a1f273034ad4d2b30ebed201a tree   34 45 1797
70d733623d440ad95d53272528ae900295855665 tree   34 45 1842
cfb98c1ce98de4290f310e81cf0b9128749df334 tree   130 129 1887
08df14d61d050a7235a5bab256e63432d422ab61 tree   64 67 2016
67ed7f289e73c67217c5bcdbe18f8f55c2d3699f tree   34 45 2083
8205fc9052e9544796e762459e48909f661145dc tree   64 67 2128
4c1ae63f5abfcb9107241f03a86df80aafbc6780 tree   34 45 2195
a8c1a9640eb8e53987cd5684535769a9f86622cf tree   64 67 2240
b7391b72f9c25ce53cf9a3679f9157d19897fd5c tree   34 45 2307
022d4f657214d39e3d540d214aa7affdd44cf489 tree   96 100 2352
69f3e7095dc70df5693bb6c213bdc77d3665be3d tree   96 100 2452
8a776b73f34c3b546a66fe6173dfa0a53a142b9b tree   34 45 2552
26f76fe87ba045f5a6b40d93598ca96e5a1fab39 tree   64 67 2597
c50a174948973a2dbe8a43fd9282d24a7a6074c4 blob   15 24 2664
9d791ce077105e227ffae01975a81e980a06a9a2 blob   24 34 2688
6d1f7671b90551cb98157a48a7b26b1183dfb821 tree   27 40 2722 1 cfb98c1ce98de4290f310e81cf0b9128749df334
1ea40765c24dc3a9109c06d2e2c1408ea40568be blob   25 35 2762
e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 blob   0 9 2797
d00491fd7e5bb6fa28c517a0bb32b8b506539d4d blob   2 11 2806
41ba6a00583e910f220ecd34021d4d29de0feeda blob   3 12 2817
896691a3909edace46e4b587e1f1496af22f36d7 tree   4 15 2829 1 022d4f657214d39e3d540d214aa7affdd44cf489
ce7499ae1967bd055f42c5458a60be5131f85da2 blob   21 31 2844
258be8efcad31cc0c6c8dc6bc7d15a2c6910cd1a blob   22 32 2875
840bbdd49116cbffa896bb7f5ab011f2ddf8d446 blob   19 29 2907
b8947b77094228836f18792dc5fac15dfa9de11e blob   15 24 2936
8cbab3b672a04bc8bf5fa63af6e06b0d92bd4126 tree   4 15 2960 1 69f3e7095dc70df5693bb6c213bdc77d3665be3d
非 delta:32 個對象
鏈長 = 1: 11 對象
.git/objects/pack/pack-20f597c6ab0c05f5c907023edf4e282be00ad6fe.pack: ok

同樣的.git/refs/下面的引用文件也會被打包,這裏可以看到該目錄已經清空了:

tree .git/refs/heads
.git/refs/heads

0 directories, 0 files

它們會被打包到.git/packed-refs文件中,可以直接用cat命令查看:

cat .git/packed-refs
# pack-refs with: peeled fully-peeled sorted
3cb383a390cbd7cbf4872412b828987e0cdc1b13 refs/heads/develop
3cb383a390cbd7cbf4872412b828987e0cdc1b13 refs/heads/master
312f01ad223d5eb1e959122ac2829744b59fd7f2 refs/tags/v1.0
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章