Git 子模块 (Submodule) 完全使用指南
Git 子模块 (Submodule) 完全使用指南
核心原理解析:子模块是什么?
想象一下,你在开发一个大项目(父项目),需要用到另一个独立的项目(例如一个公共库、一个UI组件库)。你不想直接复制粘贴它的代码,因为那样就无法方便地获取那个库的后续更新。
Git 子模块就是来解决这个问题的。它允许你将一个 Git 仓库作为另一个 Git 仓库的子目录。
最重要的核心原理:
父项目不存储子模块的所有文件内容。它只存储一个“指针”,这个指针精确地指向子模块仓库的某一个特定的 Commit ID。
这就是理解所有子模块操作的关键。当你更新、切换父项目的分支时,这个“指针”会跟着改变,指向子模块在那个时间点应该使用的版本。
下面会分 3 部分讲述子模块的使用技巧,操作方式包括 Git 命令行方式 和 TortoiseGit 客户端方式
第一部分:添加一个新的子模块
场景:
你的主项目 MyProject
需要引入一个外部库 SharedLibrary
。
💻 命令行操作
-
添加子模块
在你的父项目MyProject
的根目录下,执行以下命令:# git submodule add <仓库URL> <存放路径> git submodule add https://github.com/user/SharedLibrary.git libs/shared-library
这个命令做了三件事:
- 克隆
SharedLibrary
仓库到libs/shared-library
目录。 - 创建一个
.gitmodules
文件(如果它不存在的话),用来记录子模块的信息。 - 将子模块的当前最新 Commit 添加到父项目的暂存区。
- 克隆
-
提交父项目的变更
运行git status
,你会看到:new file: .gitmodules new file: libs/shared-library
现在,提交这个变更来正式将子模块“注册”到你的父项目中:
git commit -m "feat: Add SharedLibrary as a submodule" git push
🐢 TortoiseGit 操作
- 在父项目
MyProject
的根目录空白处右键,选择 TortoiseGit -> Submodule Add… (添加子模块)。 - 在弹出的对话框中:
- Repository: 填入子模块的仓库 URL (例如
https://github.com/user/SharedLibrary.git
)。 - Path: 填入你想存放子模块的本地路径 (例如
libs/shared-library
)。 - Branch: 可以留空,默认会使用远程仓库的默认分支。
- Repository: 填入子模块的仓库 URL (例如
- 点击 OK。TortoiseGit 会自动执行添加操作。
- 操作完成后,你会发现父项目有文件变更。右键父项目 -> Git Commit…。
- 在提交窗口,你会看到新增的
.gitmodules
文件和libs/shared-library
文件夹。填写提交信息,然后 Commit & Push。
第二部分:克隆与更新含有子模块的项目
场景:
你的同事需要克隆 MyProject
,或者你需要拉取包含了子模块更新的父项目。
💻 命令行操作
-
首次克隆项目
使用--recurse-submodules
参数,可以在克隆父项目时自动初始化并拉取所有子模块。git clone --recurse-submodules <父项目的URL>
如果你忘记加参数,也可以分步操作:
git clone <父项目的URL> cd MyProject git submodule update --init --recursive
--init
: 初始化本地的.git/config
文件,注册子模块信息。--recursive
: 如果子模块还包含了其他子模块,也一并处理。
-
拉取父项目的更新 (可能包含子模块指针的变更)
日常工作中,你拉取了父项目的更新,发现它指向了子模块的一个新版本。# 1. 拉取父项目的变更 git pull# 2. 根据父项目最新的指针,更新子模块的代码 git submodule update --recursive
🐢 TortoiseGit 操作
-
首次克隆项目
- 右键 -> Git Clone…
- 在克隆对话框中,填入 URL 和目录后,勾选 “Recursively clone submodules” (递归克隆子模块)。
-
拉取父项目的更新
- 右键父项目 -> Git Sync… -> 点击 Pull 拉取父项目更新。
- 拉取后,右键父项目 -> TortoiseGit -> Submodule Update… (更新子模块)。
- 在对话框中,确保 “Initialize submodules” (初始化子模块) 被勾选,然后点击 OK。
第三部分:修改、提交和推送子模块
由于子模块默认处于“分离头指针”(Detached HEAD)状态,我们必须先将子模块切换到分支(主分支 or 你指定的分支)后,才能进行修改。
注:在“分离头指针”这种状态下,你依然可以提交代码,但这些提交不属于任何分支。当你试图用 git push origin HEAD 推送时,Git 懵了:
- 你 (Source): “把 HEAD 指向的这个 commit 推上去!”
- Git (Destination): “推到远程仓库 (origin) 的哪里去呢?HEAD 在远程不是一个合法的分支名。我不知道该在远程创建/更新哪个分支。”
场景:
你需要修复 SharedLibrary
子模块里的一个 Bug。
💻 命令行操作
-
进入子模块并切换到分支
# 1. 进入子模块目录 cd libs/shared-library# 2. 检查状态,你会看到 "HEAD detached at..." git status# 3. 切换到你想修改的分支(例如 main) git checkout main# 4. (强烈推荐) 拉取最新代码,确保你的修改基于最新版本 git pull
-
修改、提交并推送子模块
现在你可以像操作任何普通 Git 仓库一样操作它。# ... 在这里修改文件 ... git add . git commit -m "fix: 修复了 SharedLibrary 中的某个重要 Bug" git push origin main
至此,子模块本身的代码已经推送到它自己的远程仓库了。但父项目还不知道这个变化。
-
更新父项目的指针
# 1. 回到父项目根目录 cd ../..# 2. 检查状态,你会看到子模块被标记为 "modified (new commits)" git status # 输出会像这样: # modified: libs/shared-library (new commits)# 3. 添加这个变更到暂存区 git add libs/shared-library# 4. 提交并推送父项目的更新 git commit -m "chore: 更新 SharedLibrary 子模块以修复 Bug" git push
🐢 TortoiseGit 操作
-
进入子模块并切换到分支
- 在文件浏览器中,右键子模块文件夹 (
libs/shared-library
)。 - 选择 TortoiseGit -> Switch/Checkout…。
- 在对话框中,选择 “Branch”,然后从下拉菜单中选择
main
分支,点击 OK。 - (推荐) 再次右键子模块文件夹 -> Git Sync… -> Pull。
- 在文件浏览器中,右键子模块文件夹 (
-
修改、提交并推送子模块
- 在子模块文件夹内修改代码。
- 右键子模块文件夹 -> Git Commit -> ‘main’…。
- 填写提交信息,勾选文件,点击 Commit。
- 在弹出的成功对话框中,点击 Push。确认推送信息后,点击 OK。
-
更新父项目的指针
- 回到父项目根目录。
- 右键父项目文件夹空白处 -> Git Commit…。
- 在提交窗口,你会看到
libs/shared-library
被列为“已修改”。 - 勾选它,填写提交信息(例如:“chore: 更新子模块”),然后点击 Commit。
- 在成功对话框中,点击 Push,将父项目的更新推送到远程。
总结与最佳实践
- 核心牢记: 父项目只记录子模块的一个 Commit ID。
- 双重提交: 修改子模块永远是两步操作:先在子模块内部
commit & push
,再回到父项目commit & push
更新指针。 - 进入再改: 修改子模块前,永远先
cd
进去,然后git checkout
到一个分支。 - 及时更新: 拉取父项目更新后,记得运行
git submodule update
来同步子模块代码。 - 沟通: 如果团队协作,当你更新了子模块指针并推送到父项目后,一定要通知其他成员,让他们知道需要运行
git submodule update
。
希望这篇详尽的教程能帮你彻底掌握 Git 子模块的使用!