🌱 引言 · 为什么今天一定要学会 submodule?
在真实的软件世界里,我们很少只面对“一个仓库、一条分支、一套代码”的童话。
微服务、共享组件、跨团队 SDK、甚至不同语言的构建脚本,都可能散落在独立的 Git 仓库里。于是,“把别人的仓库嵌进我的仓库”成了日常刚需——
• 前端团队把 Design-System 当子模块挂在 web-app 里;
• 固件工程师把第三方驱动仓库塞进主芯片 SDK;
• DevOps 把 Terraform 模块仓库嵌进基础设施 mono-repo;
• 甚至一本用 Markdown 写的大部头技术书,也把每章示例代码独立成子仓库,方便读者单独克隆。
如果每次都手动复制粘贴,版本就会迅速失控:今天 A 仓库升级了 API,B 仓库却还停留在旧 ABI,连编译都过不了。
Git 给出的官方答案就是 submodule —— 让主仓库只保留“指针”,真实代码仍由子仓库自己管理,既解耦又能精确追踪版本。
然而 submodule 的 UX 常被吐槽“反人类”:
clone 后代码是空的、update 命令一长串、指针冲突看不懂、CI 拉不动……
本篇《把子模块驯成乖猫咪》正是在这样的背景下写成:
把踩坑经验翻译成一步步可复制的命令,把晦涩概念画成猫咪洗澡换毛的比喻,让你十分钟就能从“为什么”走到“怎么做”,再到“怎么做得优雅”。
带着“让跨仓库协作不再痛苦”的使命,我们开始吧。
🌊 《把子模块驯成乖猫咪:Git Submodule 从入门到精通的实战魔法书》 🐱🏍
—— 一条命令就让它更新,一行配置就能同步,一口气读完就能上手!
📚 目录
- 概念速览:Submodule 到底是什么?
- 环境准备:初始化一个“主仓库”与“子仓库”
- 添加子模块:第一次把猫咪带回家
- 克隆含子模块的仓库:别让猫咪跑丢
- 本地更新子模块:猫咪想换新毛
- 批量更新所有子模块:一键换毛
- 提交子模块指针变更:告诉主仓库“猫换毛了”
- 进阶技巧:
• 固定子模块到指定分支
• 递归克隆/拉取 alias
• 子模块冲突排查 - 一键脚本:懒人专属
- 参考资料 & 彩蛋
1️⃣ 概念速览
- 主仓库(Superproject):你的大项目。
- 子模块(Submodule):存在于主仓库里,却指向另一个独立 Git 仓库的某个提交。
- 指针文件:主仓库只记录子模块的“提交 SHA”,不记录代码本身。
一句话:子模块像猫咪,主仓库只是猫窝,猫咪自己会去别处洗澡换毛。
2️⃣ 环境准备
# 创建两个干净的练习仓库
mkdir ~/submodule-lab && cd ~/submodule-lab
# 1. 子仓库(library)
git init library
cd library
echo "# Library README" > README.md
git add . && git commit -m "Initial library commit"
cd ..
# 2. 主仓库(app)
git init app
cd app
echo "# App README" > README.md
git add . && git commit -m "Initial app commit"
3️⃣ 添加子模块:第一次把猫咪带回家
# 仍在 ~/submodule-lab/app
git submodule add ../library vendor/library # 使用相对路径,方便本地演练
git status
# 会看到:
# new file: .gitmodules
# new file: vendor/library
git commit -m "Add library as submodule"
.gitmodules
文件内容:
[submodule "vendor/library"]
path = vendor/library
url = ../library
4️⃣ 克隆含子模块的仓库:别让猫咪跑丢
# 换台电脑/目录模拟
cd ~/submodule-lab
git clone app app-clone
cd app-clone
# 关键!
git submodule update --init --recursive
# 或一步到位:
git clone --recurse-submodules ../app app-clone-fast
5️⃣ 本地更新子模块:猫咪想换新毛
# 进入子模块目录
cd vendor/library
git checkout main # 确保在正确分支
git pull origin main # 拉取最新代码
# 或手动做修改
echo "new feature" > new.txt && git add . && git commit -m "Add new feature"
# 返回主仓库
cd ../..
# 查看主仓库状态
git diff --submodule
# 看到子模块指针从旧 SHA 变成新 SHA
git add vendor/library
git commit -m "Bump library to latest main"
6️⃣ 批量更新所有子模块:一键换毛
# 顶层目录
git submodule foreach 'git fetch origin && git checkout main && git pull origin main'
# 或者直接递归拉取
git submodule update --remote --merge
--remote
:让 Git 读取.gitmodules
中记录的跟踪分支并拉取。--merge/--rebase
:决定如何整合上游变更。
7️⃣ 提交子模块指针变更:告诉主仓库“猫换毛了”
git status
git add .
git commit -m "Update all submodules to latest upstream"
git push
CI/CD 提示:如果主仓库有钩子,请确保子模块的 URL 对 CI 可见(SSH key 或 token)。
8️⃣ 进阶技巧
8.1 固定子模块到指定分支
.gitmodules
追加:
[submodule "vendor/library"]
branch = stable
然后:
git submodule set-branch --branch stable vendor/library
git submodule update --remote
8.2 递归克隆/拉取 alias
~/.gitconfig
添加:
[alias]
cloneall = clone --recurse-submodules
pullall = !git pull && git submodule update --remote --recursive
8.3 子模块冲突排查
场景:主仓库 A 同事把子模块指到 SHA1,你指到 SHA2。
git pull --rebase # 此时出现冲突
git mergetool # 如果配置了子模块 diff 工具
# 或手动:
git checkout --theirs vendor/library # 保留对方指针
git checkout --ours vendor/library # 保留我方指针
git add vendor/library && git rebase --continue
9️⃣ 一键脚本:懒人专属
保存为 update-submodules.sh
:
#!/usr/bin/env bash
set -e
echo "🐱 Updating all submodules..."
git submodule update --init --recursive --remote --merge
git add .
git commit -m "chore: sync submodules $(date '+%F %T')" || true
git push
使用:
chmod +x update-submodules.sh
./update-submodules.sh
🔖 参考资料 & 彩蛋
- 官方文档:
https://git-scm.com/book/zh/v2/Git-工具-子模块 - GitHub Cheatsheet:
https://github.github.io/training-kit/downloads/zh_CN/github-git-cheat-sheet/ - 彩蛋:
运行git config --global diff.submodule log
可以让git diff
显示子模块的提交日志而非冷冰冰的 SHA,效果如下:Submodule vendor/library 9a3e2f0..c8d4b1a: > Add new feature > Fix typo
🎉 恭喜!至此,你已掌握从“把猫咪领回家”到“给它一键洗澡”的全部魔法。
下次见,继续驯服更多 Git 神兽!
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)