↓快轉到主要內容
  1. 教學文章/

Git Partial Clone 實戰:blob:none、按需下載與離線驗證

·8 分鐘· loading · loading · ·
Git Partial Clone Blob:none Promisor Remote Lazy Fetch Offline Developer-Tools
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
版本控制: Git - 本文屬於一個選集。
§ 19: 本文

一. 前言: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 才不只是漂亮的參數,而是知道成本放在哪裡的工程選擇。

延伸閱讀
#

版本控制: Git - 本文屬於一個選集。
§ 19: 本文

相關文章

Git sparse-checkout 實戰:大型 Monorepo 的部分取出與安全切換
·7 分鐘· loading · loading
Git Sparse-Checkout Monorepo Cone Mode Partial Clone Worktree Developer-Tools
Git maintenance 實戰:用 Commit-Graph、MIDX 與排程加速大型 Repo
·10 分鐘· loading · loading
Git Git Maintenance Commit-Graph Multi-Pack-Index Performance Developer-Tools
Git Changelog 自動化:從 Commit、PR 到 Release Notes
·5 分鐘· loading · loading
Git GitHub Changelog Release Notes Automation GitHub Actions Developer-Tools
Git reset、restore、revert 實戰:選對復原工具
·9 分鐘· loading · loading
Git Reset Restore Revert Undo Version-Control Developer-Tools
Git tag 與 Release 實戰:版本標記、SemVer 與安全發佈流程
·8 分鐘· loading · loading
Git Tag Release SemVer Versioning GitHub Developer-Tools
Git rerere 實戰:記住衝突解法,讓 Rebase 與 Merge 不再重做
·8 分鐘· loading · loading
Git Rerere Merge-Conflict Rebase Merge Version-Control Developer-Tools