2241 字
11 分鐘

git diff 沒有輸出卻有修改?檢查暫存區與 HEAD 的差異

執行 git add 後,git diff 突然沒有輸出,但編輯器仍顯示修改,先看 git diff --cached。不帶參數的 diff 比較暫存區與工作檔案;stage 完成後,這兩份內容相同就會空白。 已暫存的修改仍可進入一般 commit。

還有一種更容易誤判的情況:stage 後在編輯器把內容改回原樣,git diff HEAD 也是空白,但暫存區仍保存先前的修改。要確認下次提交內容,必須看對比較的兩端。

先用唯讀命令找出是哪份快照#

在專案目錄執行:

Terminal window
git status --short
git diff --name-status
git diff --cached --name-status
git diff HEAD --name-status

依 Git diff 文件,這三個比較回答不同問題:

命令比較的兩端用途
git diff暫存區 → 工作檔案stage 後又改了什麼
git diff --cachedHEAD → 暫存區一般 commit 準備提交什麼
git diff HEADHEAD → 工作檔案磁碟上的檔案相對上次 commit 改了什麼

--staged 和 --cached 是同義選項。要看某個檔案的內容差異,把確切路徑放在 -- 後面,例如 git diff --cached -- settings.txt。

暫存區(index)保存的是 stage 當時的內容,並不會持續追蹤編輯器的新版本。Pro Git 的記錄變更章節 說明了「stage 後再編輯」為何會同時出現兩份修改。

status 的兩欄各看一個比較#

對已有追蹤、沒有未解 merge 的一般檔案,short status 第一欄描述 index 相對 HEAD,第二欄描述工作檔案相對 index:

M settings.txt
M settings.txt
MM settings.txt
?? notes.txt

這是四種不同情境,不是同一個檔案同時列出四次。M 是已 stage; M 是尚未 stage;MM 表示兩端都有修改,要分別看 cached 與一般 diff。?? 是未追蹤檔案,上面三種一般 diff 不會自動列出其內容。

如果 MM 搭配空白的 git diff HEAD,可能是磁碟內容回到 HEAD,而 index 還在另一個版本。不能據此說「Git 狀態壞掉」,也不需要先 reset 或刪除 .git。

未追蹤的新檔案,怎麼不 stage 就預覽內容?#

?? draft notes.txt 不在 index,也不在 HEAD,所以一般 diff、cached diff 與 HEAD diff 都可能空白。若只想先看新檔內容,不必為了預覽就改動暫存區;在 macOS/Linux 可用 git diff 的 no-index 模式比較空檔來源 /dev/null 與確切檔案:

Terminal window
git diff --no-index -- /dev/null 'draft notes.txt'

這裡比較的是兩個檔案系統路徑,不依 tracked 身分決定是否顯示;-- 區分選項與路徑,引號讓空白留在同一個參數。這不是將檔案加入 index,也不會讓它進入下一個一般 commit。預覽後決定要提交,才對確切檔案執行 git add -- 'draft notes.txt',再看 cached diff。

no-index 隱含 --exit-code:0 表示相同、1 表示不同,其他非零值才當命令錯誤。自動化不能因預覽新檔退出 1 就宣稱 Git 失敗,也不能用無條件忽略錯誤的寫法遮掉讀取失敗。

下面 Bash fixture 只操作新暫存 repository,保留一個已 stage 的無關修改,確認預覽沒有改動它;沒有 remote,不會推送:

Terminal window
set -eu
preview_fixture_dir="$(mktemp -d)"
git init -q -b main "$preview_fixture_dir"
cd "$preview_fixture_dir"
git config user.name Fixture
git config user.email fixture@example.invalid
printf 'baseline\n' > tracked.txt
git add -- tracked.txt
git -c commit.gpgsign=false commit -qm Baseline
printf 'staged edit\n' > tracked.txt
git add -- tracked.txt
printf 'new content\n' > 'draft notes.txt'
index_before="$(git write-tree)"
git diff --quiet -- 'draft notes.txt'
git diff --cached --quiet -- 'draft notes.txt'
git diff HEAD --quiet -- 'draft notes.txt'
if git diff --no-index -- /dev/null 'draft notes.txt' > "$preview_fixture_dir/preview.patch"; then
echo '預期新檔與空內容不同' >&2
exit 1
else
preview_status=$?
test "$preview_status" -eq 1
fi
test "$(git write-tree)" = "$index_before"
test "$(git show :tracked.txt)" = 'staged edit'
if git ls-files --error-unmatch -- 'draft notes.txt' > /dev/null 2>&1; then
echo '預期新檔仍未追蹤' >&2
exit 1
fi
test "$(cat 'draft notes.txt')" = 'new content'
printf 'no-index 預覽且 index 未變:%s\n' "$preview_fixture_dir"

這份新增範例於 2026-10-30 由維護流程在 macOS、Git 2.54.0(Apple Git-157)執行;沒有測試 Windows shell、二進位內容、自訂 diff driver 或並行寫入。輸出的 patch 保留在暫存目錄,與新檔一樣不會被這次 fixture stage。正式專案若涉及金鑰,不要把預覽內容貼到公開日誌。若檔案已 tracked,卻以為新增 gitignore 就能讓它消失,則接著看 gitignore 與已追蹤檔案的差別。

重現工作檔案改回去,暫存區仍保留修改#

以下用 Bash 執行,只在 mktemp 建立的獨立目錄操作,沒有 remote。本文維護流程於 2026-10-10 在 macOS、Git 2.54.0(Apple Git-157)執行;斷言涵蓋一般文字檔,不包含衝突、submodule 或自訂 diff driver。

Terminal window
set -eu
fixture_dir="$(mktemp -d)"
git init -q -b main "$fixture_dir"
cd "$fixture_dir"
git config user.name Fixture
git config user.email fixture@example.invalid
printf 'theme=light\n' > settings.txt
git add -- settings.txt
git -c commit.gpgsign=false commit -qm Baseline
printf 'theme=dark\n' > settings.txt
git add -- settings.txt
git diff --quiet -- settings.txt
if git diff --cached --quiet -- settings.txt; then
echo '預期暫存區應有修改' >&2
exit 1
else
test "$?" -eq 1
fi
printf 'theme=light\n' > settings.txt
git diff HEAD --quiet -- settings.txt
test "$(git status --short)" = 'MM settings.txt'
test "$(git show :settings.txt)" = 'theme=dark'
git -c commit.gpgsign=false commit -qm 'Commit staged content'
test "$(git show HEAD:settings.txt)" = 'theme=dark'
test "$(cat settings.txt)" = 'theme=light'
test "$(git status --short)" = ' M settings.txt'
printf '暫存快照檢查通過:%s\n' "$fixture_dir"

git diff --quiet 的狀態碼 0 代表沒有差異,1 代表有差異;其他非零狀態要當錯誤處理。這裡第一個比較成功,接著確認 cached 比較並非空白。

工作檔案改回 light 後,三個位置是:HEAD 為 light、index 為 dark、工作檔案為 light。兩端相同,所以 git diff HEAD 空白;一般 commit 仍提交 index 的 dark,磁碟內容則維持 light。

測試不會清掉暫存 repository,最後一行會顯示路徑,方便查看。不要把這些初始化與提交命令換成正式專案路徑。

決定要交哪個版本,再改暫存區#

先確認目前暫存的內容:

Terminal window
git diff --cached -- settings.txt
git show :settings.txt

如果準備提交的就是這一版,可以保留後續未 stage 的編輯。若應提交的是磁碟上的新版本,才再次 stage 該確切檔案:

Terminal window
git add -- settings.txt
git diff --cached -- settings.txt
git status --short

在上面 fixture 的提交前狀態,重新 stage light 會讓 index 回到 HEAD,該檔案的修改消失;這是選擇了目前工作內容,不是 Git 遺失資料。

cached diff 的預覽適用於一般、沒有路徑參數或 -a 的 commit。Git commit 文件 說明 -a 會先納入已追蹤檔案的修改與刪除,帶路徑的提交則有不同的內容選擇規則。不要看完 cached diff,卻換用另一種提交方式後仍假設結果相同。

commit —only 指定檔案,讀的是目前工作內容#

若其他工作已 stage,git commit --only -- file 會忽略其他路徑的 staged 修改,但指定路徑取自目前工作檔案。這正是 Git commit 的 —only 定義:它限定路徑,沒有把該路徑鎖在先前 stage 的版本。

假設自己的 owned.txt stage 時是 staged,後來又改成 working;另外 other.txt 已 stage。這時 cached diff 顯示自己的 staged,但指定 --only -- owned.txt 會交出 working。其他檔案的 staged 內容則留下,供後續工作提交。

以下 Bash fixture 只操作新的暫存 repository,不讀寫正式專案,也沒有 remote。它接著驗證提交範圍、提交內容與另一個檔案仍在暫存區:

Terminal window
set -eu
only_fixture_dir="$(mktemp -d)"
git init -q -b main "$only_fixture_dir"
cd "$only_fixture_dir"
git config user.name Fixture
git config user.email fixture@example.invalid
printf 'baseline\n' > owned.txt
printf 'baseline\n' > other.txt
git add -- owned.txt
git add -- other.txt
git -c commit.gpgsign=false commit -qm Baseline
printf 'staged\n' > owned.txt
git add -- owned.txt
printf 'working\n' > owned.txt
printf 'other staged\n' > other.txt
git add -- other.txt
test "$(git show :owned.txt)" = staged
git -c commit.gpgsign=false commit --only -qm 'Only owned' -- owned.txt
test "$(git show --name-only --format= HEAD)" = owned.txt
test "$(git show HEAD:owned.txt)" = working
test "$(git show HEAD:other.txt)" = baseline
test "$(git show :other.txt)" = 'other staged'
test "$(git status --short)" = 'M other.txt'
printf 'only 提交範圍檢查通過:%s\n' "$only_fixture_dir"

這份刊登範例由維護流程於 2026-10-23 在 macOS、Git 2.54.0(Apple Git-157)執行。它驗證一般追蹤文字檔的選取規則,沒有涵蓋 hook 改寫、merge 衝突或其他程序同時寫入;暫存目錄保留供檢查。

實際操作時,對自己的確切路徑先看 git diff HEAD -- owned.txt,再以相同路徑做 git commit --dry-run --only -- owned.txt 檢查預計範圍。dry-run 不建立 commit,也不凍結檔案;提交後仍須用 git show --name-only --format= HEAD 和 git show HEAD:owned.txt 驗收。新檔案要先對確切路徑執行 git add -- file,讓 Git 認識它。

不要以整個目錄代替檔案清單,也不要把 --only 當成同檔案多人編輯的隔離工具。它能排除其他路徑已 stage 的內容;同一個指定檔案內的所有目前修改,仍可能一起進入 commit。

多個 Agent 在同一專案工作時,先用 Git worktree 隔離工作目錄,再各自確認 HEAD、index 與工作檔案。即使只剩一位操作者,也應在 commit 前看暫存版本,而不是用空白的 git diff HEAD 代替驗收。

參考資料:

Git:git diff

Git:git status

Git:git commit

Pro Git:Recording Changes to the Repository

git diff 沒有輸出卻有修改?檢查暫存區與 HEAD 的差異
https://laplusda.com/posts/git-diff-empty-staged-changes/
作者
Zero
發佈於
2026-10-10
許可協議
CC BY-NC-SA 4.0