项目越来越大,你开始想复用别人的仓库:比如把一套公共组件库放进自己的项目里,或者把配置文件模板单独维护。直接把代码复制进来,更新时痛苦;用包管理器,又不灵活。Git 的子模块(submodule) 就是为"仓库嵌仓库"设计的。本文讲透子模块的添加、克隆、更新、删除,再补充 .gitignore、别名和 stash 几个高频实用技巧。
1. 子模块是什么
子模块允许你在一个 Git 仓库里引用另一个 Git 仓库的某个提交。父仓库不保存子仓库的代码,只记录一行信息:"这个目录对应哪个仓库、哪个提交"。这样做的好处:
- 子项目可以独立演进、独立发布版本;
- 父仓库的历史里不会塞满子项目的提交;
- 多个项目可以复用同一个子仓库,更新一处、处处受益。
代价是操作变复杂:克隆父仓库后,子模块目录是空的,需要额外两步才能把内容拉下来。先看怎么添加。
2. 添加子模块
假设你的项目要用一套公共工具库 my-tools,把它挂到 libs/my-tools 目录:
git submodule add git@github.com:用户名/my-tools.git libs/my-tools
这条命令会克隆子仓库、在 libs/my-tools 下创建一份工作副本,并生成一个 .gitmodules 文件。看看这个文件:
cat .gitmodules
# [submodule "libs/my-tools"]
# path = libs/my-tools
# url = git@github.com:用户名/my-tools.git
.gitmodules 是子模块的"通讯录",记录了每个子模块的路径和仓库地址,它本身要提交到父仓库。子模块当前指向的具体提交,则记录在父仓库的索引里:git add 和 git commit 时会把子模块的"指针"一并提交。注意:子模块的代码本身不会进父仓库,父仓库只认指针。
3. 克隆带子模块的仓库
别人克隆你的项目时,子模块默认不会被拉下来。普通克隆之后要补两步:
git clone git@github.com:用户名/项目.git
cd 项目
git submodule init
git submodule update
# 或者克隆时一步到位:
git clone --recursive git@github.com:用户名/项目.git
git submodule init 根据 .gitmodules 登记子模块,git submodule update 按父仓库记录的提交检出代码。--recursive 会递归克隆所有子模块(包括子模块的子模块)。新成员入职、CI 环境拉代码,推荐直接用 --recursive,省心。
4. 更新子模块
子模块是独立仓库,它不会自动跟随父仓库更新。进入子模块目录,把它当成普通仓库操作:
cd libs/my-tools
git fetch
git checkout v1.2.0 # 或者 git pull 到最新
cd ../..
git add libs/my-tools
git commit -m "升级 my-tools 到 v1.2.0"
关键理解:你在子模块里 checkout 或 pull 后,子模块的指针变了,但父仓库还不知道。必须回到父仓库 git add 子模块目录,把新指针提交,队友拉取后才会同步到新版。想查看所有子模块的状态,用 git submodule status:每行最前面的符号有讲究,- 表示子模块未初始化,+ 表示当前检出的提交与父仓库记录的不一致(该更新了),没有符号则是干净状态。
5. 删除子模块
子模块不用了,删除比添加啰嗦,因为要把三处痕迹都清掉:暂存区条目、.gitmodules 配置、子模块目录。Git 新版一条命令能搞定大部分:
git rm libs/my-tools
rm -rf .git/modules/libs/my-tools
git rm 会移除子模块条目和 .gitmodules 中的配置;.git/modules 里还躺着子模块的完整克隆,手动删掉才彻底。最后 git commit -m "移除 my-tools 子模块" 提交即可。
6. 高级技巧:.gitignore、别名与 stash
除了子模块,还有几个日常高频技巧。先说 .gitignore,它告诉 Git 哪些文件永远不要跟踪:
# 编译产物
*.pyc
__pycache__/
dist/
# 本地配置
.env
config.local.json
# 临时文件
*.log
.gitignore 支持通配符(*)、目录(__pycache__/)和注释(#)。注意:已经跟踪的文件不受它影响,要停止跟踪得用 git rm --cached 文件名。
再配置两个别名,把高频命令缩短:
git config --global alias.st status
git config --global alias.lg "log --oneline --graph --all"
git st
git lg
别名就是"自定义命令",git st 等价于 git status,git lg 直接画出提交历史的图形。最后是 git stash:手头改动还没做完,但要切分支去修紧急 bug,可以把改动"存起来":
git stash push -m "登录页改了一半"
git stash list
git stash pop
git stash 把工作区和暂存区的改动暂存到一边,工作目录恢复干净;git stash pop 取回最近一次暂存。注意 stash 默认只针对已跟踪文件,新文件要加 -u 参数才会一起存。
7. 总结与练习
子模块让"仓库复用仓库"成为可能:添加用 git submodule add,克隆用 --recursive,升级要"改子模块指针 + 提交父仓库"两步走,删除要清三处痕迹。配合 .gitignore、别名和 stash,日常操作会顺滑很多。练习:
- 建两个测试仓库,把其中一个作为子模块挂进另一个,走一遍添加 → 提交 → 克隆验证 → 更新 → 删除的完整生命周期。
- 给项目写一个
.gitignore,把__pycache__、node_modules、.env都忽略掉,用git status验证。 - 配好
git st、git lg别名,再练习一次git stash存、取改动。
💡 子模块适合"低频变动、需要独立版本"的依赖;如果依赖更新频繁,还是考虑包管理器。选型时想想:你是在管理代码,还是在管理依赖的版本?