在 Day 26 的文章中,我們發現跑完 git gc
後,.git/objects
為空,但是 .git/objects/info
跟 .git/objects/pack
都多了一些東西,另外還有 packed-refs
這個新檔案。
為什麼經過 git gc
指令發生這麼大的變化?在今天的文章中,我們將藉此一指令,探討 git 的「打包」機制。
首先做出第一個 commit:
echo "hello" > file1.txt
git add file1.txt
git commit -m "First commit"
再做出第二個 commit:
echo "world" > file2.txt
git add file2.txt
git commit -m "second commit"
目前 .git/
資料夾結構如下:
.git/
│
├── info/
│ └── exlcude
│
├── objects/
│ ├── 3b/ # tree物件
│ │ └── 746528b6006b476da9a7cbb97a15722e9d8604
│ ├── 89/ # commit物件
│ │ └── 1845ab61a82db6a2de9de4976629c1aefff7dd
│ ├── cc/ # blob物件
│ │ └── 628ccd10742baea8241c5924df992b5c019f71
│ ├── ce/ # blob物件
│ │ └── 013625030ba8dba906f756967f9e9ca394464a
│ ├── dc/ # tree物件
│ │ └── a98923d43cd634f4359f8a1f897bf585100cfe
│ ├── fd/ # commit物件
│ │ └── 5715a0a83acc8a22c43574223f7b15ad792ca1
│ ├── info/ # 空資料夾
│ └── pack/ # 空資料夾
│
├── refs/
│ ├── heads/
│ │ └── main # 891845ab61a82db6a2de9de4976629c1aefff7dd
│ └── tags/
│
├── COMMIT_EDITMSG # Second commit
├── config
├── description
├── HEAD # ref: refs/heads/main
└── index
物件結構圖示如下:
我們使用的指令為:
git gc
在 Day 26 文章中,我們輸入此一指令是為了「清除 git filter-branch
之後產生的懸置物件」,但這指令事實上還有另一功能:優化本地倉儲(optimize the local repository)。
我們先來觀察,下完這個指令後,.git/ 資料夾發生了什麼變化:
.git/
│
├── info/
│ ├── exlcude
│ └── refs # 新增檔案,內容為891845ab61a82db6a2de9de4976629c1aefff7dd refs/heads/main
│
├── objects/ # 物件全數消失
│ ├── info/ # 內有commit-graph與packs兩檔案
│ └── pack/ # 內有pack-2edf28d6b294b6966899a923577d6912962d1568.idx
│ # 以及pack-2edf28d6b294b6966899a923577d6912962d1568.pack
│
├── refs/
│ ├── heads/ # 空資料夾
│ └── tags/
│
├── COMMIT_EDITMSG # Second commit
├── config
├── description
├── HEAD # ref: refs/heads/main
├── index
└── packed-refs # pack-refs with: peeled fully-peeled sorted
# 891845ab61a82db6a2de9de4976629c1aefff7dd refs/heads/main
如果以 git log
來看,還看不出什麼變化:
這些新增的到底是做什麼的?
info/excludes/refs
git clone
、git fetch
與 git push
等操作,若客戶端(想成本地倉儲)要對伺服器端(想成 GitHub 等平臺上的雲端倉儲),須要靠這個檔案來看有哪些分支的參考、以及各參考分別指向什麼 commit。以本範例來說,內容為:
891845ab61a82db6a2de9de4976629c1aefff7dd refs/heads/main
表示有 main
分支這個參考、而這個參考指向 891845a...
這個 commit。
在早期 git 使用的「Dump HTTP」連線中,真的是把 info/excludes/refs
當成靜態檔案,現今使用的「Smart HTTP」則也須要這份檔案,只是處理時會更複雜一些。
objects/info/commit-graph
objects/
的第一個檔案 commit-graph
是一個二進制檔案,可輸入以下指令查看:xxd .git/objects/info/commit-graph
會得到如下結果:
00000000: 4347 5048 0101 0400 4f49 4446 0000 0000 CGPH....OIDF....
00000010: 0000 0044 4f49 444c 0000 0000 0000 0444 ...DOIDL.......D
00000020: 4344 4154 0000 0000 0000 046c 4744 4154 CDAT.......lGDAT
00000030: 0000 0000 0000 04b4 0000 0000 0000 0000 ................
00000040: 0000 04bc 0000 0000 0000 0000 0000 0000 ................
...
在記憶體位址的偏置(offset)為 00000000
時,最前面的十六進制 4347 5048
對應 ACSII 表即可得右方的 CGPH
,表示這個檔案是 commit-graph
,後面記錄的資訊包含 commit 的物件 ID (Object ID, OID) 等,且因 OID 是以詞典編纂順序(lexicographic order)排列(也就是像字典照字母排),可以用二元搜尋法快速找到最初的 commit,再透過物件關係紀錄找到其他 commit。
commit-graph
的推出,讓遍歷 commit 時不再需要一一從硬碟裡讀取、解析,而能透過濃縮過的資訊,更快找到各 commit。
objects/info/packs
以本範例而言,內容如下:
P pack-2edf28d6b294b6966899a923577d6912962d1568.pack
最開頭的 P
代表偏好的(preferred)打包檔案,通常是最新打包者;後面的 pack-2edf28d6b294b6966899a923577d6912962d1568.pack
就是存在 /objects/pack
裡的檔案。
/objects/pack
pack-2edf28d6b294b6966899a923577d6912962d1568.idx
存放打包的物件索引,以及 pack-2edf28d6b294b6966899a923577d6912962d1568.pack
存放打包的物件本身。首先輸入以下指令觀察索引:
git show-index < .git/objects/pack/pack-2edf28d6b294b6966899a923577d6912962d1568.idx
得到的結果如下:
309 3b746528b6006b476da9a7cbb97a15722e9d8604 (49186b6f)
12 891845ab61a82db6a2de9de4976629c1aefff7dd (1359ed5a)
294 cc628ccd10742baea8241c5924df992b5c019f71 (b2e567e0)
279 ce013625030ba8dba906f756967f9e9ca394464a (52941500)
384 dca98923d43cd634f4359f8a1f897bf585100cfe (d3f32cb0)
160 fd5715a0a83acc8a22c43574223f7b15ad792ca1 (27defc54)
最前面的數字如 309
、12
、294
... 是偏置,也就是記憶體存的位址資訊,中間那一長串是各物件的雜湊碼(就跟打包前在 /objects
資料夾裡看到的一樣),最後括弧內的則是核對和(checksum),也就是確保完整性的檢查碼。
以第一行範例而言,我們可以知道在 pack-2edf28d6b294b6966899a923577d6912962d1568.pack
檔案中偏置為 309
處,存放雜湊碼為 891845a...
的物件。
要查看 2edf28d6b294b6966899a923577d6912962d1568.pack
內容,則可輸入以下指令:
git verify-pack -v .git/objects/pack/pack-*.idx
輸出結果如下:
891845ab61a82db6a2de9de4976629c1aefff7dd commit 216 148 12
fd5715a0a83acc8a22c43574223f7b15ad792ca1 commit 167 119 160
ce013625030ba8dba906f756967f9e9ca394464a blob 6 15 279
cc628ccd10742baea8241c5924df992b5c019f71 blob 6 15 294
3b746528b6006b476da9a7cbb97a15722e9d8604 tree 74 75 309
dca98923d43cd634f4359f8a1f897bf585100cfe tree 37 48 384
non delta: 6 objects
.git/objects/pack/pack-2edf28d6b294b6966899a923577d6912962d1568.pack: ok
以第一行來說,每個部分分別代表:
891845ab61a82db6a2de9de4976629c1aefff7dd
:物件雜湊碼。commit
:物件種類。216
:打包、壓縮前佔多少位元組。148
:打包、壓縮後佔多少位元組。12
:偏置,跟上面 .idx
檔案每行最開始的數字一樣。最後兩行則為:
non delta: 6 objects
:這六個物件都被壓縮成一個整體、而非與其他物件的差別,因此皆為「非 delta 物件」。
.git/objects/pack/pack-2edf28d6b294b6966899a923577d6912962d1568.pack: ok
:跟 .idx
檔案比較後,確認沒問題。
.git/refs/heads/main
從最新 commit 891845a...
變空的,因為這資訊被搬到 packed-refs
了。
packed-refs
內文為如下:
# pack-refs with: peeled fully-peeled sorted
891845ab61a82db6a2de9de4976629c1aefff7dd refs/heads/main
第一行註解 pack-refs with: peeled fully-peeled sorted
的 peeled fully-peeled
表示如果有 tag 標籤指向別的 tag 標籤,則這裡存的都是已經追蹤到各 tag 標籤最終會指到的 commit;sorted
則表示裡面的每個參考都已經排序好,有利使用二元搜尋法加快搜尋速率。
第二行 891845ab61a82db6a2de9de4976629c1aefff7dd refs/heads/main
就跟原本的 .git/refs/heads/main
概念相同,表示 main
分支正指向 891845a...
這個 commit。
總結來說,就是把 .git/refs/heads/main
原本應存的資訊,經過剝離(peel)與排序後,放到 packed-refs
,而剝離與排序流程可讓搜尋變快。
使用 git gc
優化本地倉儲,可將原本鬆散的物件(loose objects)打包,減少佔用的儲存空間外,也可增進搜尋特定物件的效率。
打包後的變化包含:
info/excludes/refs
:與使用 HTTP 連線有關資訊。objects/
:變空的,因為原本的鬆散物件都被打包、搬到其他地方。objects/info/
:出現 commit-graph
與 packs
檔案,前者讓找到 commit 過程變快、後者則為打包後的檔案列表。/objects/pack
:存有 .pack
為打包後的物件、另有 .idx
索引檔案供查詢物件位址。.git/refs/heads/main
:變空,因為分支指向 commit 之資訊被搬到 packed-refs
。packed-refs
:存放原本在 .git/refs/heads/main
中的資訊,但物件經過剝離與排序。打包的主要功能有二:一是減少佔用的儲存空間、二是加快搜尋目標物件的效率,本文僅針對 git gc
指令帶來的變化簡介 git 打包機制,但其實 git 還有如 git repack
等其他方式打包,如果想知道更多打包細節,可以先從 git 官方文件介紹開始認識。