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

执行完这条命令,会发生三件事:

  1. Git 会克隆 shared-lib 仓库到 libs/shared 目录
  2. 主仓库根目录下会生成一个 .gitmodules 文件
  3. 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 的配置里还残留着记录。

正确的删除步骤:

  1. 删除子模块目录:git rm -r libs/shared
  2. 删除 .gitmodules 中对应的条目(或者直接删掉整个文件)
  3. 删除 Git 内部配置:git config --remove-section submodule.libs/shared
  4. 提交变更

如果你用的是 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 就能一键初始化所有子模块。

知识体系图

下面这张图帮你理清子模块的核心操作流程:

Git Submodule 核心操作流程 主仓库 .gitmodules 文件 子模块工作目录 记录路径和URL 引用特定commit 常用操作 添加子模块 更新子模块 删除子模块 克隆含子模块项目 git submodule add git submodule update git submodule deinit + rm git clone --recurse-submodules 💡 子模块记录的是 commit 哈希,不是分支名

这张图展示了子模块的核心流程:主仓库通过 .gitmodules 文件记录子模块的路径和远程地址,同时在工作目录中引用子仓库的某个具体 commit。添加、更新、删除和克隆是四个最常用的操作。

总结一下:子模块是个好东西,但用不好也会让人头疼。我的建议是——能用包管理器解决的依赖,优先用包管理器。子模块更适合那些需要源码级控制、或者需要频繁修改的共享库。记住一点:子模块是「引用」而不是「复制」,这个思维转变很重要。


公众号:蓝海资料掘金营,微信deep3321