iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
Software Development

深入一點點認識 Git系列 第 27

Day 27-深入一點點認識 Git:透過 git gc 指令觀察 git 的打包機制

  • 分享至 

  • xImage
  •  

在 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

物件結構圖示如下:
https://ithelp.ithome.com.tw/upload/images/20250926/201785134ATMxdUSVu.png

開始打包

我們使用的指令為:

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 來看,還看不出什麼變化:
https://ithelp.ithome.com.tw/upload/images/20250926/20178513tc5KJJRblR.png

這些新增的到底是做什麼的?

  • info/excludes/refs
    git 使用超文本傳輸協定(HyperText Transfer Protocol, HTTP)進行如 git clonegit fetchgit 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)

最前面的數字如 30912294... 是偏置,也就是記憶體存的位址資訊,中間那一長串是各物件的雜湊碼(就跟打包前在 /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 sortedpeeled 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)打包,減少佔用的儲存空間外,也可增進搜尋特定物件的效率。

打包後的變化包含:

  1. info/excludes/refs:與使用 HTTP 連線有關資訊。
  2. objects/:變空的,因為原本的鬆散物件都被打包、搬到其他地方。
  3. objects/info/:出現 commit-graphpacks 檔案,前者讓找到 commit 過程變快、後者則為打包後的檔案列表。
  4. /objects/pack:存有 .pack 為打包後的物件、另有 .idx 索引檔案供查詢物件位址。
  5. .git/refs/heads/main:變空,因為分支指向 commit 之資訊被搬到 packed-refs
  6. packed-refs:存放原本在 .git/refs/heads/main 中的資訊,但物件經過剝離與排序。

打包的主要功能有二:一是減少佔用的儲存空間、二是加快搜尋目標物件的效率,本文僅針對 git gc 指令帶來的變化簡介 git 打包機制,但其實 git 還有如 git repack 等其他方式打包,如果想知道更多打包細節,可以先從 git 官方文件介紹開始認識。

參考資料

  1. gitrepository-layout - Git Repository Layout
  2. git-gc
  3. 10.4 Git Internals - Packfiles
  4. Git - Packfiles: How They Optimize Your Git Repository for Performance & Storage
  5. http-protocol last updated in 2.34.0
  6. commit-graph last updated in 2.43.0
  7. Git Commit-graph
  8. git-pack-objects
  9. Unpacking Git packfiles

上一篇
Day 26-深入一點點認識 Git:git filter-branch 可以改寫歷史,但還不足以完全清除檔案紀錄
系列文
深入一點點認識 Git27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言