當同一個檔案同時有目標修改與未完成編輯,而互動式 hunk 又無法乾淨切開時,真正要控制的不是工作樹檔案,而是 Git index 中準備成為下一個提交的 blob。本文把這項操作縮小成安全暫存庫中的可重現實驗:從 HEAD 取出基準內容、在外部建立候選位元組、寫入物件資料庫、更新 index,最後同時證明 staged 快照正確且工作樹未被碰觸。
這個意圖不是版本控制治理,也不是合併或重構衝突處理。它專注於 git hash-object、git update-index --cacheinfo、stage 0 index entry 與雙向 diff 的底層行為,適合需要逐位元控制暫存內容、保留 CRLF、BOM 或檔尾無換行狀態的維護者。
核心答案:index 是下一次提交的候選快照
精準部分暫存的可靠做法,是先把欲提交的完整檔案內容建成獨立 blob,再只改 index 對該路徑的指向;工作樹繼續保存所有編輯,提交候選與開發現場因此能分開檢查。
一般教學常把 staging 描述成「把修改放進暫存區」,容易讓人誤以為 index 只記錄差異。實際上,對一般檔案而言,index entry 記錄模式、物件 ID、路徑與衝突階段等資訊;物件 ID 指向完整 blob。提交時建立的 tree 會根據 index 形成,而不是直接掃描工作樹。因此,精準 staging 的問題可以重新表述為:「我要讓這個路徑在 index 指向哪些位元組?」
這個模型也解釋了三個容易混淆的觀察面。git diff 比較工作樹與 index,顯示尚未暫存的差異;git diff --cached 比較 index 與 HEAD,顯示預計提交的差異;git show :path 直接讀出 index 中 stage 0 的 blob。三者回答不同問題,不能只看其中一份輸出便宣稱隔離完成。
對可被 git add -p 清楚拆分的普通 hunk,應優先使用 porcelain 指令,因為互動流程較容易閱讀。只有當兩個意圖落在同一 hunk、行尾與編碼必須逐位元保存,或自動化需要決定性輸入時,才值得使用 plumbing。底層能力不是日常捷徑,而是處理模糊邊界的精密工具。
建立候選 blob 前的邊界與風險
動手前必須固定三個邊界:基準是目前 HEAD、目標是 stage 0 的一般檔案、工作只在隔離暫存庫執行。若存在未解衝突、特殊檔案模式或屬性過濾器,直接套用範例可能得到不同位元組。
第一個邊界是檔案模式。一般非執行檔常見 100644,可執行檔可能是 100755,符號連結則有不同模式。不要憑印象填入固定值;實務上可用 git ls-files --stage -- path 或 git ls-tree HEAD -- path 讀取既有模式。本篇沙箱故意使用 100644,因為 fixture 是一般文字檔,這不是所有專案都適用的預設答案。
第二個邊界是內容過濾。git hash-object --no-filters 會依提供的位元組建立 blob,不套用屬性機制選出的 clean filter 或行尾轉換。這正適合「候選檔已經是應進入 index 的規範位元組」的實驗;若候選檔仍是工作樹表示法,則要評估 --path 讓 Git 依目標路徑套用過濾。兩種做法不可混用,也不能在不了解 .gitattributes 時假設雜湊輸入等於工作樹檔案。
第三個邊界是同時寫入。index 是共享狀態;另一個 Git 程序在檢查與更新之間改變 index,會讓先前觀察失效。執行前後都應重新查看 git status --short 與 git ls-files --stage,不要在多人共用工作樹中把範例當成無鎖交易。本文的實測使用新建暫存 repo,沒有借用任何既有專案或在途編輯。
還要明確保留復原路徑。若候選快照不正確,可用 git restore --staged -- path 讓 index 的該路徑回到 HEAD,而工作樹不會因此被還原。不要用會覆寫整個工作樹的命令處理單一路徑 staging 錯誤,也不要在檢查前提交。
可重現步驟:寫 blob 並更新 index
流程分成基準、候選、登錄與讀回四段;每一段都產生可比較的物件或輸出。以下命令只應在自己建立的沙箱中執行,路徑與作者資料都是無效示例值,不代表任何真實環境。
- 建立新的暫存 repo,加入一個含 CRLF 且檔尾無換行的基準檔,完成單一基準提交;接著建立含
TARGET與UNRELATED的工作樹版本。 - 從基準位元組建立只加入
TARGET的候選檔,保留原有 CRLF 與檔尾無換行;計算操作前的工作樹雜湊。 - 以
hash-object -w --no-filters寫入候選 blob,再用update-index --cacheinfo只更新sample.txt的 index entry。 - 以
git show :sample.txt讀回 staged 位元組,分別和候選檔及操作後工作樹比較;最後檢查 cached 與 uncached diff。
git init sandbox-index-lab
cd sandbox-index-lab
git config user.name "Index Snapshot Lab"
git config user.email "index-snapshot@example.invalid"
: "建立並提交基準 sample.txt 後,再準備候選檔 snapshot_candidate.bin"
before=$(git hash-object --no-filters sample.txt)
blob=$(git hash-object -w --no-filters snapshot_candidate.bin)
git update-index --cacheinfo "100644,$blob,sample.txt"
after=$(git hash-object --no-filters sample.txt)
git show :sample.txt > staged_actual.bin
cmp snapshot_candidate.bin staged_actual.bin
cmp working_expected.bin sample.txt
test "$before" = "$after"
git diff --cached --no-color -- sample.txt
git diff --no-color -- sample.txt
hash-object -w 的 -w 很重要:它不只計算 ID,也把物件寫進物件資料庫。update-index --cacheinfo 接受的物件必須可被後續 tree 與提交讀取;只製造看似合理的字串並不能建立有效快照。命令採單一參數形式 mode,object,path,可避免三個值被 shell 邊界錯誤拆分。
候選內容應由可讀腳本產生,而不是在命令列臨時拼接。二進位寫入能明確控制 CRLF、BOM 與最後一個位元組,也讓測試 fixture 可重跑。若直接用文字模式編輯器,平台行尾正規化可能讓「只新增一行」變成整檔差異,視覺上難以辨識,物件 ID 卻已完全不同。
驗證:同時證明 staged 正確與工作樹未變
完整驗證必須提出兩份證據:index 讀回值等於候選快照,且操作前後工作樹雜湊相同。只看到 cached diff 有目標行,仍不足以排除未暫存內容被覆寫或候選含多餘變更。
本次在安全暫存 repo 中,以固定的 git version 2.38.1.windows.1 執行可讀腳本。fixture 的 staged 候選只新增 TARGET,工作樹另外保留 UNRELATED。實際結果為:候選 blob baaff5c1da309ae9ac3d54e3f1303a300e1af71a;操作前後工作樹雜湊皆為 f52f6115e1031348ce4a98b664be416030cde9f6;staged 與 unstaged 各有一個 hunk;兩份位元組比較與兩份 diff check 全部通過。這些結果只描述該 fixture,不推論其他 repo 必然成功。
驗證時可依序回答下列問題。第一,git show :sample.txt 是否逐位元等於候選檔?第二,git hash-object --no-filters sample.txt 在 index 更新前後是否相同?第三,git diff --cached 是否只呈現目標內容?第四,git diff 是否仍呈現刻意保留的未暫存內容?第五,git ls-files --stage 的模式與路徑是否符合預期?任何一項不成立,都應停止提交並復原該路徑的 index。
CRLF 需要額外說明。Git 的一般 whitespace 檢查可能把 carriage return 視為行尾空白;當測試目的就是保存 CRLF 時,可在命令上使用 -c core.whitespace=cr-at-eol 進行檢查,而不是改掉 fixture。這項設定只調整該次檢查如何解讀 CR,不會證明其他空白規則都正確,所以仍要保留位元組比較。
若檔案含 BOM,還可比較候選與 staged 讀回值的前幾個位元組;若檔尾無換行,diff 通常會顯示對應提示,但最可靠的判定仍是 cmp 或雜湊一致。人眼 diff 適合檢查意圖,位元組比較負責檢查表示法,兩者互補而非互斥。
常見失敗、診斷與安全復原
最常見失敗不是 Git 無法寫入 blob,而是候選快照選錯基準、套錯過濾器、填錯模式,或只驗證 staged 一側。診斷時應先讀回 index 與工作樹,再決定是否復原,不要用提交掩蓋錯誤。
若 cached diff 顯示整個檔案被改寫,先檢查候選的行尾與編碼,再檢查是否錯把工作樹表示法配上 --no-filters。若 git show :path 不是候選內容,查看 git ls-files --stage -- path,確認更新的是正確路徑與 stage 0。若工作樹雜湊改變,表示除了 index 手術外還有其他程序寫檔;停止操作,保留現場並重建隔離實驗,不要直接判斷是哪個工具造成。
安全復原單一路徑可使用 git restore --staged -- sample.txt。執行後,git diff --cached -- sample.txt 應為空,git diff -- sample.txt 則重新顯示工作樹相對 HEAD 的全部修改。這正好再次證明 restore 的目標是 index,而不是把工作樹退回基準。若 repo 正處於合併衝突,stage 1、2、3 有特殊意義,本篇 stage 0 流程不適用,應改用衝突處理程序。
不要把 git update-index 與 assume-unchanged 或 skip-worktree 混為一談。那些旗標處理的是工作樹檢查或稀疏簽出相關行為,不是建立精準提交內容的替代方案。隱藏變更也不是隔離變更;真正可稽核的做法,是讓 index 內容可讀回、可比較、可復原。
提交前最後再執行一次 git status --short、cached diff 與 uncached diff,並確定沒有不相干路徑進入 index。若目標修改會影響建置或測試,還要在候選快照對應的 tree 上執行相關檢查;本文的沙箱只驗證 Git 物件與 index 行為,沒有宣稱驗證任何應用程式。
檢查清單與使用時機
只有在互動式暫存無法表達所需邊界,而且團隊能審查候選位元組時,才應採用 index plumbing。一般修改仍以 git add -p 為首選,底層手術保留給行尾、編碼或同 hunk 意圖隔離等特殊情況。
- 先確認工作位於隔離 repo 或獨立 worktree,並記錄 Git 版本。
- 從明確基準建立候選完整內容,不直接覆寫工作樹。
- 從現有 index 或 tree 讀取檔案模式,不盲填
100644。 - 了解
.gitattributes與 clean filter 後,再選擇--no-filters或--path。 - 用
git show :path讀回 staged blob,做逐位元比較。 - 比較操作前後工作樹雜湊,證明 index 更新沒有寫檔。
- 同時審查
git diff --cached與git diff,確認兩側各自保留正確意圖。 - 提交前確認其他路徑未被暫存,並執行與修改相符的建置或測試。
- 發現錯誤時只復原目標路徑的 index,不使用破壞工作樹的手段。
這套方法的價值不在於命令較低階,而在於它把「我想只提交這一部分」轉成可驗證的資料契約:候選 blob 是精確輸入,index entry 是明確指向,cached 與 uncached diff 是兩個觀察面,工作樹雜湊則是未被碰觸的證據。當每一項都有可重跑的判定,部分暫存才從視覺判斷升級為可稽核快照。