跳至主要内容

[CLI] Git Submodule

TLDR

git submodule update --init --recursive # 對準主專案記錄版本

git submodule update --remote # 對準子專案最新 commit

常用指令

指令功能
git submodule add https://github.com/someone/shared-utils.git libs/shared-utils把子專案加入主專案的指定路徑。
git submodule init初始化 .gitmodules 內的 submodule 設定。
git submodule update [--init] [--recursive]拉取 submodule 內容;可初始化,並連同 submodule 裡面的 submodule 一起更新。
git clone --recurse-submodules https://github.com/you/fastapi-project.gitclone 專案時一併拉取所有 submodule。

更新 submodule

兩種更新方向

指令行為
git submodule update(不加 --remote把子專案拉回主專案記錄的那個 commit(往回對齊指標)
git submodule update --remote把子專案推進到遠端分支的最新 commit(主動升級指標)

同一個 update,差一個 --remote,方向剛好相反。一個是「跟隨主專案」,一個是「跟隨上游」。這個差異是很多人 submodule 用到崩潰的根源。

設定 --remote 要追蹤的分支

.gitmodules 這個檔案除了記「路徑 -> URL」,還可以多記一個 branch 欄位。你可以手動編輯它,或用指令設定:

git submodule set-branch --branch main libs/shared-utils
# main 是分支;最後一段是路徑

設定完,.gitmodules 裡那一段會長這樣:

[submodule "libs/shared-utils"]
path = libs/shared-utils
url = https://github.com/someone/shared-utils.git
branch = main

這個 branch 欄位的用途,就是告訴 --remote 系列指令:「當我要追上游時,請追這個分支的最新版本。」如果不設定,預設是追 main(舊版 Git 是 master)。

情境 A:升級到上游最新版本

正確的、不用手動 cd 進去 fetch 的流程是:

git submodule update --remote libs/shared-utils # 自動追 main 最新,省掉手動 cd + fetch + pull
git add libs/shared-utils # 一樣要把新指標 stage 起來
git commit -m "Bump shared-utils to latest main" # 一樣要 commit 這行新指標

--remote 可以指定單一 submodule,也可以不指定路徑來更新全部:

# 只升級這一個 submodule
git submodule update --remote libs/shared-utils

# 不指定路徑,升級主專案裡的全部 submodule
git submodule update --remote

--remote 省掉的是手動 cd 進 submodule 後再 fetchpull 的步驟。它省不掉、也絕對不能忘的,是最後那兩步:回到主專案把新的 submodule 指標 addcommit 起來。少了這兩步,其他人拉主專案時還是會拿到舊版 submodule。

情境 B:修改子專案並推回遠端

如果不是單純把 submodule 更新到上游最新版本,而是要實際修改子專案程式碼,流程要分成「子專案內」和「主專案」兩個階層。

進子專案,先切到分支

cd libs/shared-utils
git checkout main # 跳出 detached HEAD,站到真正的分支上

這一步是為了避免在 detached HEAD 狀態下修改。先切到真正的分支上,後面才能正常 commit 和 push。

在子專案內修改、commit、push

# ...修改檔案...
git add .
git commit -m "Fix bug in shared-utils"
git push origin main # 推回子專案自己的遠端

這裡的 git push 推的是子專案自己的遠端 repo,跟主專案無關。在這個當下,子專案就是一個獨立 repo,你做的是一般 Git 操作。

這個 push 一定要做。如果你只在本地 commit 卻沒有 push,主專案等等記錄的新 submodule commit 對其他人來說會不存在。他們執行 git submodule update 時,就可能抓不到那個 commit 而報錯。

順序也很重要。如果反過來,主專案的指標會指向一個子專案遠端還沒有的 commit。原因是子專案的 commit 還只在本地,不在遠端;在這段時間差裡,如果其他人 pull 主專案並更新 submodule,就會抓不到那個 commit。因此會建議先推子專案(submodule),再推主專案。

回主專案,commit 新指標

cd ../..
git status # 會看到 modified: libs/shared-utils (new commits)
git add libs/shared-utils
git commit -m "Update shared-utils with bug fix"
git push # 推主專案

新增 submodule

# 第一段是 URL;第二段是路徑
git submodule add https://github.com/someone/shared-utils.git libs/shared-utils

這個指令會把子專案放到 libs/shared-utils,並在主專案裡記錄 .gitmodules 和 submodule 指標。新增後記得把這些變更一起 commit 起來。

整個流程會像這樣:

你 add 時指定路徑

路徑寫進 .gitmodules 的 path 欄位

.gitmodules 被 commit、推到遠端

別人 clone --recurse-submodules 時,Git 讀 .gitmodules,照著 path 把子專案放到那個位置

submodule 的位置在 add 的那一刻就由你決定,並寫進 .gitmodules;之後所有人的 clone 都只是照表抄錄。