一. 前言:Clone 很快,為什麼 git show 突然要上網?
#
你替大型 repo 加上 --filter=blob:none,初次 clone 終於不必等到咖啡變冷。
隔天在沒有網路的地方想看舊版檔案,Git 卻開始連遠端,最後報錯。
這不是它忘記下載,而是你選擇了「需要時再拿」的工作模式。
Partial clone 的承諾是延後部分物件傳輸,不是離線資料完整。
拍拍君今天不用巨大 monorepo,而用兩個 commit、三個 blob,親手驗證這個差別。
之前的 sparse-checkout 教學處理「哪些檔案展開到工作目錄」;本文則追問「檔案內容是否真的存在本機」。兩個問題不能互相代答。
二. 先拆開三種「只拿一部分」 #
| 方法 | 控制哪一層 | 常見誤解 |
|---|---|---|
--depth=1 |
歷史深度 | 當前版本的大檔仍可能全部下載 |
--filter=blob:none |
物件傳輸 | 不代表永遠不下載 blob |
| sparse-checkout | 工作目錄展開範圍 | 不會自動清掉已下載的 Git objects |
Commit 保存歷史關係,tree 描述目錄與檔名,blob 保存檔案內容。
對沒有再加 shallow 或 ref 限制的 blob:none clone,Git 仍取得所選 refs 的 commit/tree 歷史,把 blob 留到需要時補拿。
一般 clone 接著會 checkout;checkout 需要目前版本的檔案內容,因此可能立刻補回一大批 blob。
要隔離量測初次物件傳輸,以下刻意使用 --no-checkout。
這些選項的定義可對照 git-clone 官方文件。
三. 建立可重現、無外部網路的實驗 #
Partial clone 是 Git 內建功能,不用安裝 Python 套件。
先確認版本;本文使用 Git 2.50.1,指令以 Bash 執行:
git --version
bash
以下都在新的暫存目錄操作,不要拿現有專案測試:
lab_dir=$(mktemp -d)
cd "$lab_dir"
git init -b main source
cd source
git config user.name '拍拍君'
git config user.email 'pypy@example.invalid'
mkdir archive
printf '# Pypy\n' > README.md
printf 'version one\n' > archive/report.txt
git add .
git commit -m 'first report'
printf 'version two\n' > archive/report.txt
git add archive/report.txt
git commit -m 'update report'
這裡共有三個不同的 blob:README、第一版 report、第二版 report。
接著建立只供本機實驗使用的 bare remote:
cd "$lab_dir"
git clone --bare source remote.git
git -C remote.git config uploadpack.allowFilter true
git -C remote.git config uploadpack.allowAnySHA1InWant true
remote_url="file://$lab_dir/remote.git"
git clone --filter=blob:none --no-checkout "$remote_url" partial
cd partial
uploadpack.allowAnySHA1InWant 在此只是讓隔離實驗能直接按 OID 補拿物件;不要把這份 server 設定照抄到正式服務,正式環境應遵循主機平台的物件存取政策。
為什麼不是 git clone ../remote.git?
直接使用本機路徑可能走 local clone 最佳化;file:// 才讓這個實驗經過支援 filter 的 Git 傳輸流程。
四. Promisor Remote:誰答應之後補貨? #
檢查剛建立的 repo:
git config --bool --get remote.origin.promisor
git config --get remote.origin.partialclonefilter
git rev-parse --is-shallow-repository
git rev-list --count HEAD
預期依序得到 true、blob:none、false、2。
promisor 表示 Git 把這個 remote 視為可補拿缺漏物件的來源,不是「傳輸一定成功」的網路保證。
再看 Git 管理的 pack 標記:
pack_dir=$(git rev-parse --git-path objects/pack)
find "$pack_dir" -name '*.promisor' -print
.promisor 和對應 pack/index 一起保留來源語意,讓 Git 區分預期缺漏與真正損壞。
不要手動刪除它,也不要為了讓 repo「看起來正常」就移除 promisor 設定。
遠端是否仍可連線、憑證是否有效、舊物件是否仍能取得,都會影響日後的補貨。
概念背景可看 Git partial clone 設計文件。
五. 建立缺漏清單,但別一邊檢查一邊下載 #
有些「檢查物件」指令本身就會觸發 lazy fetch,結果你一量測,狀態就改變了。
這裡用 local-only 模式搭配明確的 missing 行為:
GIT_NO_LAZY_FETCH=1 git rev-list \
--objects --all --missing=print > ../missing-before.txt
awk '/^\?/ {n++} END {print n+0}' ../missing-before.txt
實驗預期是 3。
? 開頭的行代表 traversal 遇到的缺漏物件;不要把整份輸出行數當成缺漏數量。
GIT_NO_LAZY_FETCH=1 告訴 Git 不要在物件查找時自動向 promisor remote 補拿。
它不是一般用途的網路防火牆:你若明確執行 git fetch,仍是在要求網路操作。
這份清單以本機 refs 可達物件為範圍,不包含從未 fetch 的遠端 branch、已不可達歷史或其他儲存系統。
本文用 blobless repo,tree 已在本機;若改成缺 tree 的 filter,無法沿缺失 tree 繼續走訪,不能直接套用同樣的完整性推論。
選項細節見 git-rev-list 文件與 Git 環境變數。
六. 觀察一次 Lazy Fetch #
先從本機 tree 取得最新 report 的 OID,再問它是否真的存在:
report_oid=$(git rev-parse HEAD:archive/report.txt)
if GIT_NO_LAZY_FETCH=1 git cat-file -e "$report_oid"; then
printf 'blob 已在本機\n'
else
printf '知道 OID,但本機還沒有 blob\n'
fi
知道物件名稱,不等於擁有物件內容。
現在正常讀取:
GIT_TRACE=1 git show HEAD:archive/report.txt
你會讀到 version two;trace 可以觀察到 Git 為缺漏物件啟動 fetch 的過程。
再查一次:
GIT_NO_LAZY_FETCH=1 git rev-list \
--objects --all --missing=print > ../missing-after.txt
awk '/^\?/ {n++} END {print n+0}' ../missing-after.txt
本實驗從 3 降到 2:剛才需要的 blob 已補回,README 與舊版 report 仍不在本機。
正式遠端可能多傳一些物件,所以不要把「永遠恰好減一」寫成通用測試契約。
反覆讀取互不相干的歷史內容,也可能變成許多小型請求;clone 省下的時間,有時只是移到之後的 diff、blame 或 review。
七. 離線驗證:同一份 Repo,兩種答案 #
關閉 lazy fetch 後,剛讀過的版本依然能看:
GIT_NO_LAZY_FETCH=1 git show HEAD:archive/report.txt
但尚未下載的舊版應該失敗:
if GIT_NO_LAZY_FETCH=1 git show HEAD~1:archive/report.txt; then
printf '舊版已在本機\n'
else
printf '預期失敗:舊版內容尚未下載\n'
fi
這是可重現的「物件不可用」測試,不必真的拔網路線。
若要連自動補拿的失敗路徑一起測,本機實驗還能暫時禁止 file transport:
git -c protocol.file.allow=never show HEAD~1:archive/report.txt
這條預期回傳非零;設定只作用於這次指令,不會改寫 repo config。
正式離線驗收則應在受控的無網路環境執行實際工作;不要把 local-only Git 檢查等同整個 build 都不需要網路。
八. 預熱任務,不是只預熱 Commit 清單 #
假設離線 review 需要目前 checkout 與上一版 report,先在線上完成真正會讀 blob 的操作:
git show HEAD~1:archive/report.txt > ../report-old.txt
git switch --detach HEAD
注意:這個切換只適合目前沒有 checkout、也沒有本機工作的實驗 repo。正式工作區應先確認修改已保存。
接著重跑 local-only 驗證:
GIT_NO_LAZY_FETCH=1 git show HEAD~1:archive/report.txt
GIT_NO_LAZY_FETCH=1 git rev-list \
--objects --all --missing=print > ../missing-ready.txt
awk '/^\?/ {n++} END {print n+0}' ../missing-ready.txt
這個小實驗會得到 0,因為三個 blob 都剛好被用過。
大型 repo 不會因此全部補齊;你要列出實際需要的 refs、commit 範圍、檔案與指令,再執行離線驗收。
例如「只看 commit 標題」不需要和「顯示 patch」一樣的內容;git log --oneline 成功,不代表 git log -p 一定成功。
同理,git rev-list --objects ... > /dev/null 只是走訪與列舉,不是下載所有列出的 blob;不能拿它當離線預熱程序。
九. 怎麼量測,才不會自己騙自己? #
請在同一個 remote、同一組 refs、相同 checkout 條件下比較:
cd "$lab_dir"
/usr/bin/time -p git clone --no-checkout "$remote_url" full-measure
/usr/bin/time -p git clone --filter=blob:none --no-checkout \
"$remote_url" partial-measure
git -C full-measure count-objects -vH
git -C partial-measure count-objects -vH
小實驗用來看行為,不拿幾個文字檔宣稱「效能提升十倍」。
正式量測至少分成三段:初次 clone、首次工作集 checkout、典型歷史查詢。
記錄各段時間、.git/objects 的 pack 大小、缺漏數與成功率;count-objects 是儲存量資訊,不是精確網路流量計。
需要分析協定時,可在自己的測試 repo 使用 GIT_TRACE_PACKET=1;trace 可能含 repo 路徑與 ref 名稱,不要直接公開整份紀錄。
如果 server 印出 filtering not recognized by server, ignoring,就不能把這次結果當作有效 filtered 傳輸。
本機實測也確認:停用 server 的 filter 後,clone 會警告並取得全部 blob。
只看到 remote.origin.partialclonefilter=blob:none 不足以證明節省成功;要一起看協定、警告、缺漏清單與實際傳輸結果。
十. fsck 成功,不代表可以當完整備份
#
在仍有 promised missing blobs 的階段,以下可能照樣成功:
cd "$lab_dir/partial-measure"
GIT_NO_LAZY_FETCH=1 git fsck --full
這是因為 fsck 理解 partial clone 的預期缺漏;它沒有替你證明「所有內容都存在本機」。
可把驗收拆成三層:
| 驗收 | 回答的問題 |
|---|---|
fsck |
這份 repo 是否符合 Git 理解的完整性規則? |
| Local-only 缺漏盤點 | 本機 refs 的可達物件是否仍有已知缺漏? |
| 無網路任務測試 | 指定 review、build 或 test 是否真的能完成? |
即使缺漏盤點為零,也只涵蓋選定 refs;Git LFS 內容、submodule repo、套件快取與容器映像是另外的資料來源。
要長期封存,較容易稽核的方式是另建無 filter、無 shallow 限制的獨立 clone,確認 refs 範圍,再做完整性與還原測試。
不要只複製一份 partial clone 的 .git,就宣布備份完成。
十一. 改 Filter、Repack 與 GC 的界線 #
改 filter 是調整之後的傳輸策略,不是把已下載的內容「退貨」。
例如希望改成只延後大型 blob,可以先在獨立測試 clone 評估:
git config remote.origin.partialclonefilter 'blob:limit=1m'
git fetch --refetch origin
--refetch 會重新請求符合 filter 的物件集合,可能有顯著傳輸成本;一般增量 fetch 並不等於重套 filter 或補齊全部舊缺漏。
這也不是把 partial clone 轉成完整 clone 的一鍵指令;仍需重新盤點與驗證。
維護則交給理解 promisor 的 Git,不要自行拼接 pack 或刪除標記:
git fsck --full
git count-objects -vH
本文另在仍缺兩個 blob 的實驗 clone 執行 git repack -ad 與 git gc,驗證缺漏清單、promisor 標記與後續補拿仍正常;這不是建議你在正式 repo 盲目照跑。
現行 git-repack 文件說明 promisor packs 會分開處理,因此也別把它簡化成「GC 永遠不碰 promisor packs」。
若目標只是讓日常查詢更快,請讀Git maintenance、Commit-Graph 與 MIDX,不要把索引最佳化和物件可用性混成同一個問題。
結語:延後下載,是一份要驗收的契約 #
拍拍君會用 partial clone 加速「連線穩定、只用少數工作集」的開發環境,但不會讓它默默扮演離線完整備份。
導入時先問三件事:遠端真的支援 filter 嗎?哪些指令會補拿 blob?失去網路後,工作還做得完嗎?
把 clone、首次讀取與離線測試分開量測,blob:none 才不只是漂亮的參數,而是知道成本放在哪裡的工程選擇。