Monorepo 很方便:API、前端、CLI、部署設定和共用 library 都能在同一份歷史裡協作。直到某天,你只想改 services/payments/,卻得面對幾十萬個完全用不到的檔案。
IDE 索引慢、檔案監看器一直忙、搜尋結果被別的團隊淹沒,連 git status 都像在巡視一座城市。
這時候,拍拍君會拿出 Git 內建的 sparse-checkout:Repository 仍然是完整的 Git repository,但 working tree 只展開你真正需要的部分。
這篇把它整理成可以安全切換、檢查與復原的日常工作流。
一. sparse-checkout 到底省了什麼? #
先把三件容易混在一起的事分開:
| 技術 | 主要減少 | 沒有保證減少 |
|---|---|---|
| sparse-checkout | Working tree 裡展開的檔案 | .git 裡既有的物件 |
| shallow clone | 下載的歷史深度 | 當前版本的檔案數量 |
| partial clone | 一開始下載的 Git objects | Working tree 自動變小 |
sparse-checkout 的核心問題是:「這個 commit 裡有哪些路徑,應該出現在我的 working tree?」它不是權限系統,也不會把其他目錄從 commit 歷史中刪掉。
只要物件已經在本機,你仍然可以用 Git 指令查看未展開路徑的內容:
git show HEAD:services/catalog/README.md
因此,不要拿 sparse-checkout 隔離機密;它是開發體驗與 I/O 範圍工具,不是安全邊界。
二. 建立可重現的示範 Monorepo #
Git 已經內建 sparse-checkout,不必另外安裝套件。先確認版本與子命令:
git --version
git sparse-checkout -h
接著建立小型示範 repo:
mkdir pypy-monorepo
cd pypy-monorepo
git init
mkdir -p services/api services/worker apps/admin docs tools
printf '# API\n' > services/api/README.md
printf '# Worker\n' > services/worker/README.md
printf '# Admin\n' > apps/admin/README.md
printf '# Docs\n' > docs/README.md
printf 'workspace = true\n' > workspace.conf
git add .
git commit -m '建立 monorepo 骨架'
目前 working tree 會看到全部內容;等等只保留 services/api/ 與 docs/。
三. Cone Mode:目錄導向的安全預設 #
大多數人的需求都是「我要這幾個目錄」,而不是手寫複雜 pattern;這就是 cone mode 適合的情境。
git sparse-checkout init --cone
git sparse-checkout set services/api docs
現在檢查頂層:
find . -maxdepth 3 -type f -not -path './.git/*' | sort
你會看到指定目錄,也會看到根目錄的 workspace.conf。
這不是漏網之魚。
Cone mode 會保留 repository 根目錄的檔案,並展開所選目錄以及必要的祖先結構。
對許多 monorepo 而言,根目錄的 lockfile、格式設定和 workspace config 本來就需要一起存在。
可以用這個指令查看目前選了什麼:
git sparse-checkout list
輸出應該是:
docs
services/api
這份清單比直接偷看內部 pattern 更適合作為日常檢查入口。
四. set 與 add:替換範圍還是擴大範圍?
#
兩個指令長得很像,語意卻不同。
set 會把選取範圍替換成新的集合:
git sparse-checkout set services/worker
此時 services/api 與 docs 不再屬於目標範圍。
add 則會保留既有選擇,再加上新目錄:
git sparse-checkout add tools
git sparse-checkout list
結果會包含:
services/worker
tools
拍拍君的記法很簡單:
- 換一個任務上下文:用
set - 臨時多碰一個相依模組:用
add - 不確定目前狀態:先用
list
在自動化腳本裡,通常應該偏好 set。
因為它描述的是完整期望狀態,多跑一次仍得到同一個結果;一直 add 則可能讓範圍愈來愈大。
五. 切換前先保護本機修改 #
最危險的不是 sparse-checkout 本身,而是你忘了 working tree 還有未提交內容。
先建立一個修改:
git sparse-checkout set services/api docs
printf '\nlocal note\n' >> services/api/README.md
git status --short
接著若把範圍切到 services/worker,Git 不應該默默刪掉會造成資料損失的檔案。
但「Git 會保護我」不該取代自己的切換程序。
建議每次縮小範圍前先做:
git status --short
git diff --check
git diff --stat
然後選一種保存方式:
# 方法一:完成一個可理解的 commit
git add services/api/README.md
git commit -m '補充 API 說明'
# 方法二:工作尚未完成,先暫存並包含 untracked files
git stash push -u -m '切換 sparse scope 前保存 API 工作'
想深入 stash 的選擇,可以參考Git stash 實戰。
重點不是「永遠先 commit」,而是切換前必須知道哪些資料只存在 working tree。
六. Ignored 與 Untracked 檔案為什麼還在? #
即使把 services/api 移出 sparse 範圍,Git 也不會任意刪除 ignored 或 untracked 資料。因此你可能看見一個「理論上沒有選,實際上仍存在」的目錄。
先查來源,不要直接判定 sparse-checkout 失效:
git status --short --ignored services/api
git check-ignore -v services/api/.cache/result.txt
如果確定內容可重建,再另外使用經過 dry run 的清理流程:
git clean -ndX services/api/.cache
-n 是預覽;沒有確認輸出前,不要拿掉它。清理 generated files 與調整 sparse scope 是兩個不同決策。
七. reapply:索引狀態跑掉時重新套用
#
某些 merge、rebase 或工具操作後,working tree 可能暫時出現 sparse 規則之外的 tracked files。
先檢查狀態,再重新套用:
git status --short
git sparse-checkout reapply
reapply 不是萬用修復按鈕。有修改或衝突時,Git 仍可能保留檔案;正確順序是:
- 閱讀
git status - 解決衝突或保存修改
- 執行
reapply - 再次用
list與檔案清單驗證
最後用 git sparse-checkout list 與 git status --short 驗證,不要只相信畫面看起來乾淨。
八. Non-Cone Mode:真的需要 pattern 才使用 #
有時你不是想要整個目錄,而是想展開跨目錄的特定檔案,例如所有 Dockerfile 與部署 YAML。
這時可以改用 non-cone mode:
git sparse-checkout init --no-cone
git sparse-checkout set --no-cone '/*' '!/*/' '/**/Dockerfile' '/deploy/**/*.yaml'
Non-cone patterns 類似 .gitignore 語法,能力更自由,也更難推理。
它的成本包括:
- Pattern 的先後與 negation 容易讓人誤判
- 大型 repo 的匹配成本可能更高
- 團隊成員不一定能從目錄清單理解範圍
- 腳本與人工操作更難共享同一個心智模型
所以拍拍君的建議是:
能用目錄描述需求,就留在 cone mode。
若真的要用 non-cone,請把 pattern 檔納入團隊文件與測試,而不是只存在某個人的 shell history。
九. Partial Clone:少展開不等於少下載 #
對全新 clone,若遠端支援 partial clone,可以把兩者結合:
git clone \
--filter=blob:none \
--sparse \
https://example.com/pypy/monorepo.git
cd monorepo
git sparse-checkout set services/api docs
--filter=blob:none 代表先不下載所有 blob,等實際需要時再向遠端取得。兩者分工是:
- partial clone 控制 object 傳輸
- sparse-checkout 控制 working tree 展開
某些歷史查詢、diff 或 scope 擴張可能觸發網路下載。離線工作前,至少跑一次任務需要的 build、test 與歷史查詢:
git rev-list --objects HEAD -- services/api >/dev/null
git log --oneline -- services/api | head
若 Git server 不支援 filter,不要假裝 partial clone 生效;先用實際 trace 與網路行為驗證。
十. 搭配 Worktree:每個工作區有自己的 Scope #
Git worktree 解決的是「同時開多個 branch」,sparse-checkout 解決的是「每個 working tree 展開哪些路徑」。
兩者可以搭配,每個工作區保留自己的 scope:
git worktree add ../pypy-docs -b docs-refresh
cd ../pypy-docs
git sparse-checkout init --cone
git sparse-checkout set docs
現代 Git 會使用 worktree-specific configuration 管理 sparse 狀態,避免一個 worktree 的 scope 直接覆蓋另一個。
git worktree list
git sparse-checkout list
git config --get extensions.worktreeConfig
例如主 worktree 放常用服務、review worktree 放 PR 目錄、docs worktree 只放文件。Branch 流程仍是另一層問題,可再看Git Branch 策略完全攻略。
十一. 安全停用與完整復原 #
要回到完整 working tree,不要手動改內部 pattern,直接停用:
git sparse-checkout disable
git status --short
git sparse-checkout list
停用後,list 應提示目前不在 sparse checkout 狀態,而 tracked files 會重新展開。
遇到異常時先收集資訊:
git status
git rev-parse --show-toplevel
git config --show-origin --get core.sparseCheckout
git config --show-origin --get core.sparseCheckoutCone
git config --show-origin --get extensions.worktreeConfig
接著保存修改、解決衝突、嘗試 reapply,必要時 disable 後重新 init --cone。不要第一時間刪除 index、.git 或 sparse 設定檔,那只會把 scope 問題升級成 repository 修復問題。
十二. 團隊實務:把 Scope 變成可重現介面 #
別讓每位開發者各自背一串指令。把常見角色整理成小型腳本:
#!/usr/bin/env bash
set -euo pipefail
role="${1:-}"
case "$role" in
api) paths=(services/api packages/contracts tools) ;;
admin) paths=(apps/admin packages/ui packages/contracts) ;;
docs) paths=(docs examples) ;;
*) printf 'usage: %s {api|admin|docs}\n' "$0" >&2; exit 2 ;;
esac
if ! git diff --quiet || ! git diff --cached --quiet; then
printf 'working tree 有修改,請先保存再切換 scope\n' >&2
exit 1
fi
git sparse-checkout init --cone
git sparse-checkout set "${paths[@]}"
git sparse-checkout list
這讓角色只能映射到已審查的目錄,dirty tree 時拒絕切換,最後再印出 Git 接受的範圍。CI 也能做最小驗證:
./scripts/sparse-role.sh api
test -d services/api
test -d packages/contracts
test ! -e apps/admin
test -z "$(git status --porcelain=v1)"
若根目錄檔案仍存在,不要把它當失敗;cone mode 本來就會保留它們。
十三. 上線前檢查清單 #
導入團隊工作流前,至少確認:
- Repository 的常用 scope 能用目錄表達
- 根目錄設定檔保留是預期行為
- Scope 切換前有 dirty-tree guard
- Generated、ignored 與 untracked files 有獨立清理政策
-
set、add、reapply、disable的用途有文件 - Partial clone 與 worktree 的行為已測試
- Build、test、IDE 與檔案監看器在縮小範圍後仍正常
- 團隊知道 sparse-checkout 不是存取控制
結語 #
sparse-checkout 最有價值的地方,不是讓 repository 看起來比較小,而是把開發者每天需要注意的工作範圍縮到合理大小。
先用 cone mode 與少量目錄開始,把 status → 保存修改 → set → 驗證 變成固定流程。
當 clone 傳輸真的也是瓶頸,再評估 partial clone;當需要多分支並行,再搭配 worktree。
工具一層一層加,邊界一層一層驗證,大型 monorepo 也能保持安靜。🔭