Git 版本控制 › 第 11 章

物件模型再探:packfile 與 delta 壓縮

2026-07-02  ·  packfile · delta · gc · count-objects · loose object

本章為進階/選讀內容,可依需要選讀。這是第二部(進階)的第一章,聚焦「物件在硬碟上怎麼存」的細節, 屬於原理補完——不看也完全能日常使用 Git。若你對第 2 章的物件模型意猶未盡、或好奇 git push 傳的到底是什麼,這章正好接上。
第 2 章說每顆 commit 指向一棵完整 tree、每個檔案是一個 blob——那是 Git 的邏輯模型,永遠成立。 但如果每存一次都把整個檔案內容原封不動寫一份,幾百個版本的大檔會把 .git 撐爆。 本章拆開儲存層:Git 平常把物件當「散裝檔案」(loose object)存,累積多了就打包成 packfile, 並在包裡用 delta 壓縮把相近物件存成「基準 + 差異」。關鍵是——這一切只發生在儲存層, 邏輯上 Git 仍然是一串完整快照(呼應 附錄 A.4)。

11.1散裝物件與打包:packfile 何時產生

回顧 2.5 節:每次 git add / git commit,Git 會把新產生的 blob、tree、commit 各自 zlib 壓縮後,寫成 .git/objects/ 底下一個獨立檔案——路徑就是它的 SHA (前 2 碼當資料夾、後 38 碼當檔名)。這種「一物件一檔案」的形式叫 loose object(散裝物件)

散裝很適合「剛寫入」:單筆寫入快、單筆讀取也快。但它有兩個問題:① 每個檔案各自壓縮, 同一個檔案改了 20 版就是 20 份幾乎一樣的內容,彼此之間的相似性完全沒被利用;② 檔案數量爆炸時, 光是 inode 與檔案系統開銷就很可觀。於是 Git 會在適當時機把一大堆散裝物件收攏成少數幾個 packfile

散裝物件散落在 .git/objects/ab/cdef…;打包後收進一個 .pack(內容)+一個 .idx(索引)。點按鈕模擬:
loose object 打包成 packfile 示意圖 左側散落多個獨立壓縮的散裝物件檔案,執行 git gc 後收攏成右側一個 .pack 與一個 .idx 檔案 .git/objects/(散裝) a1 b7 c3 d9 e2 f5 08 每個物件各自 zlib 壓縮、各佔一個檔案 git gc 打包 pack-3f9c…​.pack 7 個物件收在一起 相近物件以 delta 壓縮 pack-3f9c…​.idx 索引:SHA → 在 .pack 的位移
圖 11-1散裝物件經 git gc 收攏成一個 .pack(內容)+一個 .idx(索引);沒有索引就無法在包裡快速定位某個 SHA
目前是散裝狀態:7 個物件各佔一個檔案。點「執行 git gc」看它們被打包。

packfile 是一對檔案,住在 .git/objects/pack/:

檔案內容
pack-<hash>.pack所有被打包物件的實際內容,相近物件之間用 delta(見 11.2)壓縮
pack-<hash>.idx索引檔:記錄每個物件的 SHA 對應到 .pack 裡的哪個位移(offset),讓 Git 不必掃整包就能直接跳到某物件

打包(以及順帶的清理)通常在這些時機自動或手動發生:

時機說明
git gc(手動)Garbage Collect:把散裝物件打包、合併小 packfile、清掉過期 reflog 與真正無人參照的物件
自動 gc許多指令(commitmerge…)結束後,若散裝物件或 pack 數量超過門檻,Git 會自動觸發 git gc --auto(門檻由 gc.auto / gc.autoPackLimit 設定)
git push / git fetch(傳輸)網路傳輸一律走 packfile:傳送端即時打一個包(對方沒有的物件)、以 delta 壓縮後傳出——這就是為什麼 clone 一個大 repo 會看到 Compressing objectsReceiving objects 兩個進度條
git repack(手動)只做重新打包,不做 gc 的其他清理工作;調參數重打包時用
「打包」不等於「刪除」。gc 打包時,原本被參照的散裝物件會被搬進 pack;而沒有任何 ref 或 reflog 指向的物件(如 9.3 節被 reflog 覆蓋後失聯的 dangling commit)才會在過期後真正被清掉。 也因此,剛「弄丟」的 commit 在 gc 前通常還救得回來——但一旦 gc 把過期的 unreachable 物件清乾淨,就真的沒了。

11.2delta 壓縮:為何存了差異,卻仍是「快照」

packfile 省空間的核心是 delta 壓縮:對內容相近的兩個物件,Git 不會兩份完整都存, 而是挑一個當 base(基準物件)存完整內容,另一個只存「相對於 base 的差異指令」(delta), 形如「從 base 複製第 0~900 個 byte、然後插入這 12 個新 byte、再複製第 950~1000 個 byte」。 要還原 delta 物件,就照著指令把 base 拼出來。

多個物件可以接力:B 以 A 為 base、C 又以 B 為 base……形成一條 delta 鏈(delta chain)。 鏈越長越省空間(每一環只存差異),但還原鏈尾的物件要先還原它前面每一環,越深越慢。 Git 用兩個參數在「省空間」與「還原速度」之間取捨:

參數(預設)意義
pack.window(10)打包時,每個物件會跟前後這個範圍內的候選物件比對,挑最省的當 base——窗口越大越可能找到好 base,但打包越慢
pack.depth(50)一條 delta 鏈最長幾環。超過就強制讓下一個物件重新存成完整 base,避免還原時要套用過多層
同一個 README.md 有 5 個版本(內容逐版遞增)。拖動滑桿改變 pack.depth(鏈長上限),看 Git 如何選 base:
4
delta 鏈與 base 選擇示意圖 五個版本的物件依 pack.depth 上限被分成完整 base 與 delta,delta 以箭頭指向所依賴的 base
圖 11-2珊瑚色=完整 base;紫色=delta(箭頭指向它依賴的 base)。depth 上限越小,越常被迫另存完整 base(pack 變大),但還原最深物件套用的 delta 越少
完整 base 數(存整份內容)
1
delta 物件數(只存差異)
4
還原最深物件需套用
4 個 delta
別把這張圖讀成「Git 按版本順序存 diff」。真實的 base 選擇不看 commit 歷史—— Git 是把物件依類型、路徑、大小排序後,在 pack.window 窗口內找「拼起來最省」的物件當 base, base 甚至常是比較大/比較新的那個(用大檔當基準、把小差異存成 delta 更划算)。 圖裡用「版本鏈」只是為了直覺;實際上 delta 可以跨檔、跨版本,方向也不固定。
那 Git 到底算不算「存 diff」?——不算,而且分兩個層次要分清楚(這裡把 附錄 A.4 那句「老實的補充」講透):
  • 邏輯層:純快照。每顆 commit 仍指向一棵完整 tree,取任何版本都是「直接讀那份內容」, 不需要像 SVN 那樣從基準把一串 diff 累加回來。
  • 儲存層:delta 只是壓縮手法。packfile 的 delta 是二進位的複製/插入指令 (byte 層級),跟附錄 A 講的 unified diff 文字格式是兩回事;而且它對指令模型完全透明—— 你 git cat-file -p <sha> 拿到的永遠是還原後的完整內容,根本感覺不到底下是 delta。
一句話:delta 是 Git 的「壓縮」,不是 Git 的「資料模型」。資料模型永遠是一串完整快照。

11.3動手觀察:count-objectsgc

上面兩節都是原理,這節把它變成看得到的數字。想知道你的 repo 現在有多少散裝物件、佔多少空間、 打包後省了多少,靠 git count-objectsgit gc 就能觀察。

① 看目前的散裝/打包狀態:

$ git count-objects -v
count: 1203          # 散裝(loose)物件數量
size: 4820           # 散裝物件佔用空間(KiB)
in-pack: 8975        # 已在 packfile 裡的物件數量
packs: 1             # packfile 數量
size-pack: 10240     # packfile 佔用空間(KiB)
prune-packable: 0    # 已在 pack、但仍有散裝副本可清的物件數
garbage: 0           # 無法辨識的垃圾檔

count 一直往上長,代表你累積了很多還沒打包的散裝物件;跑一次 gc 通常會把 count 壓回接近 0、物件都進到 in-pack

② 手動打包並看清理結果:

$ git gc
Enumerating objects: 10178, done.
Counting objects: 100% (10178/10178), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3401/3401), done.
Writing objects: 100% (10178/10178), done.
Total 10178 (delta 6521), reused 0 (delta 0)

最後一行資訊量最大:Total 10178 是打包的物件總數,(delta 6521) 表示其中 6521 個 被存成 delta(其餘是完整 base)——delta 比例越高,通常代表這個 repo 有大量「相近版本」,壓縮效益越好。

指令用途
git count-objects -v看散裝/打包物件數與各自佔用空間(最常用的觀察指令)
git gc打包散裝物件、合併 pack、清過期物件與 reflog
git gc --aggressive用更大的窗口重新計算 delta,壓得更小但很慢——偶爾為長壽 repo 做一次即可,不必常跑
git verify-pack -v .git/objects/pack/pack-*.idx列出 pack 內每個物件的類型、大小與它的 delta 鏈深度(chain 欄),想親眼看 delta 鏈就用它
git repack -ad把所有物件重打成單一 pack 並丟棄冗餘,不做 gc 的其他清理
日常其實不需要手動 gc——Git 的自動 gc 會在背景把事情做好。真正會手動介入的場景多半是: repo 突然變很大想看看物件分佈、或做完一次大量歷史重寫(第 17 章)後想立刻回收空間。 知道「散裝 → pack → delta」這條路徑存在,遠比記住每個參數重要。

11.4本章小結

概念一句話記住
loose object剛寫入的物件各自 zlib 壓縮、各佔一個檔案,寫入快但不利用物件間相似性
packfile把大量物件收攏成 .pack(內容)+ .idx(SHA→位移索引)一對檔案
打包時機手動 gc/自動 gc(超門檻)/push·fetch 傳輸一律走 pack
delta 壓縮相近物件存成「base + 差異指令」,多個接力成 delta 鏈;window 定選 base 範圍、depth 定鏈長上限
仍是快照delta 只是儲存層的壓縮、對指令模型透明;邏輯上 Git 永遠是一串完整快照,不是存 diff
觀察工具count-objects -v 看散裝/pack 狀態、gc 打包、verify-pack 看 delta 鏈深度
承先啟後:本章把第 2 章的物件模型從「邏輯」補到「儲存」——你現在知道 .git 為何不會無限膨脹、 push 到底傳了什麼。下一章(第 12 章 · 選讀)換個方向,看工作目錄怎麼進階: worktree 讓一個 repo 同時開多個工作目錄、sparse-checkoutpartial clone 如何讓大型 repo 只抓需要的部分。