學校快開學了,孩子們的暑假已進入尾聲,工作這邊則是越來越上手,也越來越忙碌。
這週因為工作上需要處理一個較為特殊的專案結構,接觸到了 git submodule
,可能有很多人都聽過,但實際應用在專案中的機會可能不多。
在 iOS 開發的生態系中,我們早已習慣使用 CocoaPods
或 Swift Package Manager (SPM)
來管理第三方套件。它們就像是 App 開發的得力助手,幫我們處理好 Dependency
的下載、編譯與整合。那為什麼我們還需要 git submodule
呢?它和這些套件管理工具有什麼不同?又適合用在什麼樣的情境?
今天,就讓我們一起來揭開 git submodule
的神秘面紗。
什麼是 git submodule
?
簡單來說,git submodule
允許你在一個 Git repository(我們稱之為主專案 superproject
)中,嵌入另一個 Git repository(稱之為子模組 submodule
)。
這就像是在你的專案資料夾中,放入一個指向另一個專案的「書籤」。主專案本身不包含子模組的實際程式碼,它只記錄了兩件事:子模組的 repository URL,以及要使用的確切版本。
這意味著,主專案最終儲存的是一個特定的 commit hash。當然也可以設定讓子模組追蹤(track)某個特定的 branch
或 tag
。當你更新子模組時,Git 會自動抓取該分支或標籤的最新 commit,並將其 hash 記錄到主專案中。這讓版本控制變得既精確又靈活。
.gitmodules
檔案結構
當你新增 submodule 時,Git 會自動在專案根目錄建立一個 .gitmodules
檔案,記錄所有子模組的資訊:
[submodule "SharedLibrary"]
path = SharedLibrary
url = https://github.com/some/shared-library.git
branch = main
這個檔案包含三個關鍵資訊:
path
:子模組在主專案中的相對路徑url
:子模組的遠端 repository URLbranch
:要追蹤的分支(選用,預設為預設分支)
這個檔案會被納入版本控制,確保團隊成員都能取得相同的子模組設定。
submodule
vs. CocoaPods
/ SPM
既然有了方便的 SPM
和 CocoaPods
,為什麼還需要 submodule
?它們的核心差異在於「目的」與「管理方式」。

簡單來說,SPM
和 CocoaPods
更像是「消費者」模式,我們引入別人打包好的套件來使用。而 git submodule
則更像是「協作者」模式,我們將另一個我們也需要維護的專案,直接納入目前的開發流程中。
多人協作的魔法與陷阱
git submodule
在多人協作時,既是魔法,也充滿陷阱。
魔法之處
當你的團隊有多個專案需要共享一個私有的核心模組時,submodule
就非常方便。開發者可以在開發主專案的同時,直接修改 submodule
的程式碼,並將主專案與 submodule
的變更分別提交。這對於需要頻繁、同步修改主從專案的情境非常有用。
陷阱之處
然而,它的複雜性也帶來了許多協作上的挑戰:
- 忘記初始化:新成員
git clone
主專案後,submodule
的目錄會是空的。他們必須記得執行git submodule update --init --recursive
,否則專案會因為缺少檔案而編譯失敗。這是最常見的新手錯誤。 - 游離的 HEAD (Detached HEAD):當你進入
submodule
目錄時,Git 會自動切換到主專案所記錄的那個特定 commit,這會使你處於detached HEAD
狀態。如果你在這個狀態下做了修改並 commit,這個 commit 不屬於任何分支,很容易在下次更新時遺失!正確的做法是先cd
進入submodule
,git checkout
到一個分支,再進行修改。 - 更新流程繁瑣:要更新
submodule
到最新版本,你需要: cd
進入submodule
目錄,git pull
拉取最新程式碼。- 回到主專案目錄,
git add
子模組的路徑,將新的 commit hash 記錄下來。 git commit
提交這次的更新。
這個多步驟的流程很容易出錯或遺漏。
- 合併衝突:當不同分支都更新了
submodule
到不同的 commit 時,合併分支就會產生衝突,而解決這種衝突比解決一般程式碼衝突要複雜一些。 - 遠端 URL 過期 (Outdated Remote URL):如果子模組的 repository URL 變更了(例如,專案遷移到新的 Git 伺服器),單純執行
git submodule update
是不夠的。主專案的.gitmodules
檔案雖然更新了,但每個協作者的本地.git/config
仍然記錄著舊的 URL。這時就需要git submodule sync
來同步設定。
基本操作
雖然有些複雜,但掌握以下幾個核心指令,就能應付大部分場景。
# 1. 新增一個 submodule
git submodule add https://github.com/some/shared-library.git SharedLibrary
# 2. Clone 一個包含 submodule 的專案
# 方法一:Clone 後再初始化
git clone https://github.com/my/project.git
cd project
git submodule update --init --recursive
# 方法二:Clone 時一併處理
git clone --recurse-submodules https://github.com/my/project.git
# 3. 更新 submodule 到遠端的最新版本
# 方法一:自動更新所有 submodule 到其追蹤分支的最新 commit
git submodule update --remote --merge
# 方法二:手動進入 submodule 更新
cd SharedLibrary
git pull origin main
cd ..
# 將 submodule 的變更加入主專案的 stage
git add SharedLibrary
git commit -m "Update SharedLibrary to latest"
# 4. 移除一個 submodule
# 步驟較為複雜,需要清理多個地方
# 4a. 從 .gitmodules 移除設定並 stage 變更
git submodule deinit -f SharedLibrary
# 4b. 從 Git 索引移除子模組目錄
git rm -rf SharedLibrary
# 4c. 提交變更
git commit -m "Remove SharedLibrary submodule"
# 4d. 清理 .git 目錄中的殘餘檔案(可選,但建議執行)
rm -rf .git/modules/SharedLibrary
進階操作:讓協作更順暢的技巧
為了讓團隊合作更順利,特別是為了照顧對指令不熟悉的成員,可以採用以下技巧來簡化流程:
使用 Git GUI 工具
如果你不習慣使用 CLI
,許多 Git GUI 工具(如 Sourcetree、Fork、Tower 等)都內建了對 submodule 的支援。你可以透過圖形化介面來新增、更新或同步子模組,這可以大幅降低操作的複雜度,並減少人為失誤。
設定全域 Git Config
為了避免每次 pull 主專案後都要手動更新 submodule,你可以設定一個全域的 Git 設定:
git config --global submodule.recurse true
設定之後,每當你執行 git pull
時,Git 會自動幫你更新 submodule 到主專案所記錄的 commit,省去了一個步驟。
建立輔助 Scripts
對於團隊協作,建立一個簡單的 shell script 來標準化更新流程是個好方法。一個可靠的腳本應該包含 sync
和 update
兩個步驟,確保 URL 和 commit 都保持最新狀態。例如,你可以建立一個 update_project.sh
腳本:
#!/bin/sh
# update_project.sh
# 1. 拉取主專案的最新變更
echo "Pulling latest changes for the main project..."
git pull
# 2. 同步 submodule 的 URL 設定,確保本地指向正確的遠端倉庫
echo "Syncing submodule URLs..."
git submodule sync --recursive
# 3. 初始化並更新 submodule 到主專案指定的 commit
echo "Updating and initializing submodules..."
git submodule update --init --recursive
# 4. 更新所有 submodule 到各自追蹤分支的最新版本(可選)
echo "Updating all submodules to latest commits..."
git submodule foreach 'git fetch origin && git pull origin main'
# 5. 將 submodule 的變更記錄到主專案
echo "Staging submodule updates..."
git add .
git status
echo "Project and submodules are up to date!"
echo "Don't forget to commit the submodule updates if needed."
團隊成員只需要執行這個腳本,就能確保專案和所有子模組都保持在正確的狀態,避免了因操作不一致導致的問題。
結論:我該用它嗎?
那麼,回到最初的問題:我們應該在 iOS 專案中使用 git submodule
嗎?
我的建議是:除非你有非常明確的理由,否則優先選擇 SPM
。
SPM
作為 Apple 官方的套件管理工具,已經非常成熟,與 Xcode 深度整合,能滿足絕大多數 Swift/Objective-C 的依賴管理需求,無論是公開還是私有套件。它的學習曲線更平緩,協作流程也更簡單直觀。
git submodule
則是一個更底層、更強大的工具,它不受語言或生態系的限制,適用於一些特殊場景:
- 跨平台或跨語言專案:當你需要管理的模組包含非 Swift/Objective-C 的程式碼(例如 C++, Python, 或網頁前端資源),
submodule
是唯一可行的原生 Git 方案。 - 緊密的同步開發:你需要對模組進行非常頻繁的修改,並且希望將其與主專案的開發流程緊密綁定。
- 歷史專案結構:公司的專案結構有歷史因素,已經在使用
submodule
。
如果你決定使用 git submodule
,請務必確保團隊中的每個人都理解其工作流程與潛在的陷阱,並建立清晰的協作規範。否則,它帶來的麻煩可能會遠大於便利。
希望這篇文章能幫助你更了解 git submodule
,有任何疑問或想法也歡迎留言討論。
See ya!