13、Git submodule:子模块
子模块这玩意儿,说白了就是「项目里的项目」。我刚开始用 Git 那会儿,遇到一个头疼的问题:一个项目依赖另一个仓库的代码,怎么管理?直接复制粘贴?太 low 了。用包管理器?有时候又太重。后来发现了 submodule,嗯,这玩意儿真香。
核心概念:Git submodule 允许你在一个 Git 仓库中,嵌入另一个 Git 仓库的特定版本。主仓库只记录子仓库的 commit 引用,而不是直接复制代码。
子模块的概念
想象一下这个场景:你正在开发一个电商平台,前端用了一套 UI 组件库,后端依赖一个公共的 SDK。这两个库都在独立的仓库里维护。你总不能每次更新都手动复制粘贴吧?
子模块就是来解决这个问题的。它让主仓库知道:「嘿,我这里的 libs/ui-components 目录,其实是另一个仓库的某个版本。」主仓库只存一个指针——指向子仓库的某个 commit。
我个人习惯把子模块用在几个地方:
- 共享的工具库或 SDK
- 第三方依赖的源码(比如你想自己维护一个 fork)
- 大型项目中的模块化拆分
小提示:子模块和普通的文件夹不同。你用 git status 看,子模块目录显示的是绿色的「modified」——但那是子模块内部的改动,不是主仓库的改动。这个区别很重要,我一开始就搞混过。
添加子模块
添加子模块的命令很简单:
git submodule add https://github.com/example/shared-lib.git libs/shared
执行完这条命令,会发生三件事:
- Git 会克隆
shared-lib仓库到libs/shared目录 - 主仓库根目录下会生成一个
.gitmodules文件 libs/shared目录会被记录为子模块
来看看 .gitmodules 文件长什么样:
[submodule "libs/shared"]
path = libs/shared
url = https://github.com/example/shared-lib.git
这个文件要提交到版本控制里。为什么?因为别人克隆你的项目时,需要知道子模块从哪里拉。
注意:添加子模块时,默认会拉取子仓库的默认分支(通常是 main 或 master)的最新 commit。但子模块记录的是具体的 commit 哈希,不是分支名。这意味着——子模块是「锁定」在某个版本的。
更新子模块
更新子模块有两种场景:一种是更新到子仓库的最新版本,另一种是更新到指定的某个版本。
场景一:更新到最新
cd libs/shared
git pull origin main
cd ..
git add libs/shared
git commit -m "更新子模块到最新版本"
或者用一条命令搞定:
git submodule update --remote libs/shared
场景二:更新到指定版本
cd libs/shared
git checkout v2.1.0
cd ..
git add libs/shared
git commit -m "锁定子模块到 v2.1.0"
我曾经在项目里踩过一个坑:团队里有人用 git submodule update --remote 更新了子模块,但没注意子仓库的 API 有破坏性变更。结果 CI 构建直接挂了。从那以后,我建议团队:更新子模块后,一定要跑一遍完整的测试。
最佳实践:子模块的更新应该像依赖版本升级一样谨慎。每次更新后,确保主项目代码兼容新版本,再提交。
删除子模块
删除子模块比添加稍微麻烦一点。很多人以为直接 rm -rf 删掉目录就行了——嗯,我刚开始也这么干过,结果发现 .gitmodules 和 Git 的配置里还残留着记录。
正确的删除步骤:
- 删除子模块目录:
git rm -r libs/shared - 删除
.gitmodules中对应的条目(或者直接删掉整个文件) - 删除 Git 内部配置:
git config --remove-section submodule.libs/shared - 提交变更
如果你用的是 Git 1.8.5 以上版本,可以用一条命令搞定:
git submodule deinit -f libs/shared
git rm -f libs/shared
然后手动清理 .gitmodules 文件中的对应条目,再提交。
注意:git submodule deinit 会清空子模块的工作目录,但不会删除 .gitmodules 中的记录。所以最后一步手动清理是必须的。
注意事项
用子模块这些年,我总结了几条血泪教训:
| 问题 | 说明 | 建议 |
|---|---|---|
| 克隆项目后子模块为空 | 直接 git clone 不会自动拉取子模块内容 |
用 git clone --recurse-submodules 或克隆后执行 git submodule update --init |
| 子模块处于「游离 HEAD」状态 | 子模块默认不跟踪分支,切到子模块目录会看到 detached HEAD | 如果需要开发,建议在子模块内创建分支再工作 |
| 多人协作时子模块版本不一致 | 有人更新了子模块但没提交主仓库的引用变更 | 每次更新子模块后,记得在主仓库执行 git add 并提交 |
| 子模块嵌套 | 子模块里还可以有子模块,管理复杂度指数级上升 | 尽量避免超过两层的嵌套,否则你会疯的 |
小技巧:如果你经常需要处理子模块,可以在 .gitconfig 里加个别名:
[alias]
sup = submodule update --init --recursive
这样每次拉取项目后,执行 git sup 就能一键初始化所有子模块。
知识体系图
下面这张图帮你理清子模块的核心操作流程:
这张图展示了子模块的核心流程:主仓库通过 .gitmodules 文件记录子模块的路径和远程地址,同时在工作目录中引用子仓库的某个具体 commit。添加、更新、删除和克隆是四个最常用的操作。
总结一下:子模块是个好东西,但用不好也会让人头疼。我的建议是——能用包管理器解决的依赖,优先用包管理器。子模块更适合那些需要源码级控制、或者需要频繁修改的共享库。记住一点:子模块是「引用」而不是「复制」,这个思维转变很重要。
公众号:蓝海资料掘金营,微信deep3321