2026-07-02 · packfile · delta · gc · count-objects · loose object
git push
傳的到底是什麼,這章正好接上。
.git 撐爆。
本章拆開儲存層:Git 平常把物件當「散裝檔案」(loose object)存,累積多了就打包成 packfile,
並在包裡用 delta 壓縮把相近物件存成「基準 + 差異」。關鍵是——這一切只發生在儲存層,
邏輯上 Git 仍然是一串完整快照(呼應 附錄 A.4)。
回顧 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(索引)。點按鈕模擬:
git gc 收攏成一個 .pack(內容)+一個 .idx(索引);沒有索引就無法在包裡快速定位某個 SHApackfile 是一對檔案,住在 .git/objects/pack/:
| 檔案 | 內容 |
|---|---|
pack-<hash>.pack | 所有被打包物件的實際內容,相近物件之間用 delta(見 11.2)壓縮 |
pack-<hash>.idx | 索引檔:記錄每個物件的 SHA 對應到 .pack 裡的哪個位移(offset),讓 Git 不必掃整包就能直接跳到某物件 |
打包(以及順帶的清理)通常在這些時機自動或手動發生:
| 時機 | 說明 |
|---|---|
git gc(手動) | Garbage Collect:把散裝物件打包、合併小 packfile、清掉過期 reflog 與真正無人參照的物件 |
| 自動 gc | 許多指令(commit、merge…)結束後,若散裝物件或 pack 數量超過門檻,Git 會自動觸發 git gc --auto(門檻由 gc.auto / gc.autoPackLimit 設定) |
git push / git fetch(傳輸) | 網路傳輸一律走 packfile:傳送端即時打一個包(對方沒有的物件)、以 delta 壓縮後傳出——這就是為什麼 clone 一個大 repo 會看到 Compressing objects 與 Receiving objects 兩個進度條 |
git repack(手動) | 只做重新打包,不做 gc 的其他清理工作;調參數重打包時用 |
reflog 覆蓋後失聯的 dangling commit)才會在過期後真正被清掉。
也因此,剛「弄丟」的 commit 在 gc 前通常還救得回來——但一旦 gc 把過期的 unreachable 物件清乾淨,就真的沒了。
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:
pack.window 窗口內找「拼起來最省」的物件當 base,
base 甚至常是比較大/比較新的那個(用大檔當基準、把小差異存成 delta 更划算)。
圖裡用「版本鏈」只是為了直覺;實際上 delta 可以跨檔、跨版本,方向也不固定。
git cat-file -p <sha> 拿到的永遠是還原後的完整內容,根本感覺不到底下是 delta。count-objects 與 gc
上面兩節都是原理,這節把它變成看得到的數字。想知道你的 repo 現在有多少散裝物件、佔多少空間、
打包後省了多少,靠 git count-objects 與 git 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 的其他清理 |
| 概念 | 一句話記住 |
|---|---|
| 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 鏈深度 |
.git 為何不會無限膨脹、
push 到底傳了什麼。下一章(第 12 章 · 選讀)換個方向,看工作目錄怎麼進階:
worktree 讓一個 repo 同時開多個工作目錄、sparse-checkout 與 partial clone
如何讓大型 repo 只抓需要的部分。