MapleCheng

在浩瀚的網路世界中無限潛水欸少年郎!

0%

Git Submodule 刪除的隱藏陷阱:為什麼 .gitmodules 突然空了

這兩天我在整理專案的 Git submodule,想把某些 submodule 的追蹤移除但保留 .gitmodules 的設定。聽起來很直覺對吧?git rm -r 那個目錄就好了。

結果我在兩個不同的 repo 犯了一模一樣的錯。兩次。

背景:為什麼要移除 submodule 追蹤

我們專案裡有一些 submodule 是開發工具的設定檔目錄(像是 .claude/),這些目錄的內容因人而異,不應該被 Git 追蹤。正確的做法是在 .gitmodules 裡加上 ignore = all,讓 Git 忽略這些 submodule 的變更。

但問題來了:如果你在加 ignore = all 之前,這些 submodule 的 entries 已經被 Git index 追蹤了,光加 ignore 沒用——你還得把 index 裡的 entries 清掉。

所以我的計畫很簡單:

  1. 先在 .gitmodules 加上 ignore = all
  2. 再用 git rm 移除那些 submodule 的 index entries
  3. Commit,收工

災難是怎麼發生的

我跑了這條指令:

1
git rm -r .claude/

看起來完全合理。.claude/ 是我要移除追蹤的 submodule 目錄,git rm -r 移除它的 index entries。

然後我 git status 一看——

.gitmodules 的內容被清空了。

不是刪除,是清空。檔案還在,但裡面所有 submodule 的設定全部不見了。包括那些我根本沒打算動的 submodule。

為什麼會這樣

這是 git rm 對 submodule 的特殊行為。當你 git rm 一個 submodule 目錄時,Git 不只移除 index entry,它還會自動清理 .gitmodules 中對應的 section

如果你的 .gitmodules 裡只有那一個 submodule,整個檔案就被清空了。如果有多個,只有對應的 section 會被移除——但這依然不是你想要的行為,因為你可能只是想停止追蹤內容,不是想徹底移除 submodule 的定義。

更要命的是,這個行為是靜默的。沒有警告,沒有提示,git rm 就這麼做了。你要是沒仔細看 git diff,直接 commit 了,那 .gitmodules 就真的回不來了(好吧,可以 revert,但你懂那種「剛才搞了什麼」的心情)。

我怎麼在第二個 repo 又犯了一次

這才是最丟臉的部分。

第一個 repo 搞完,我花了好一番功夫 revert、reset,最後弄乾淨了。心想「好,學到教訓了」。

然後換到第二個 repo,要做一模一樣的事——

1
git rm -r .claude/

對,我又打了同一條指令。同一個錯。

我甚至知道上一個 repo 出了什麼問題,但手比腦快,肌肉記憶直接敲下去了。等我反應過來的時候,.gitmodules 又空了。

這大概就是所謂的「知道」和「做到」之間的鴻溝。

正確做法

如果你想移除 submodule 的 index tracking 但保留 .gitmodules 設定,正確的步驟是:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Step 1: 移除 submodule 的 index entries(這會動到 .gitmodules)
git rm -r .claude/

# Step 2: 立刻把 .gitmodules 還原
git reset HEAD .gitmodules
git checkout -- .gitmodules

# Step 3: 確認 .gitmodules 內容完好
cat .gitmodules

# Step 4: 只 stage 你要的變更
git add .claude/ # 這時候 .claude/ 已經不在 index 了,add 不會做什麼
git commit -m "chore: remove submodule tracking"

關鍵在 Step 2git rm 之後立刻用 git reset HEAD + git checkout --.gitmodules 還原到原本的狀態。

或者,你也可以用 --cached 搭配更精準的操作:

1
2
3
4
5
6
7
# 只移除 index,不動工作目錄和 .gitmodules
git rm -r --cached .claude/

# 但注意:--cached 對 submodule 的行為也可能動到 .gitmodules
# 所以保險起見,還是搭配 reset + checkout
git reset HEAD .gitmodules
git checkout -- .gitmodules

另一個方案:直接編輯 index

如果你不想冒任何風險動到 .gitmodules,可以用更底層的指令:

1
2
3
4
5
6
7
# 列出 .claude/ 下所有 index entries
git ls-files --stage .claude/

# 逐一移除
git update-index --force-remove .claude/commands.md
git update-index --force-remove .claude/settings.json
# ...

這個方法完全不會碰 .gitmodules,但比較囉嗦。適合你非常確定要保留 .gitmodules 原封不動的場景。

完成後的驗證

不管用哪種方法,commit 之前一定要檢查:

1
2
3
4
5
6
7
# 確認 .gitmodules 內容正確
git diff --cached .gitmodules
# 應該要沒有輸出,或只有 ignore = all 的變更

# 確認 submodule entries 已從 index 移除
git ls-files .claude/
# 應該要沒有輸出

學到的教訓

這件事讓我深刻體會到幾個道理:

Git submodule 的操作語意跟你想的不一樣。 git rm 對一般檔案就是移除 index entry,但對 submodule 它會額外處理 .gitmodules。這種「聰明」的行為在你不知情的時候就是個地雷。

犯過的錯不代表不會再犯。 我在第一個 repo 學到了教訓,轉身在第二個 repo 又踩了一樣的坑。知識要變成習慣需要時間,或者需要一個 checklist。

Git 的 submodule 系統設計得就是不直覺。.gitmodules.git/config.git/modules/ 三個地方同時管理 submodule 的狀態,任何操作都可能產生你意料之外的副作用。如果可以的話,認真考慮用 git subtree 或其他替代方案。

如果這篇能讓你在 git rm submodule 之前多想一秒,那我的臉就沒白丟了 😂