Git SubModule使用
本次补全(对照《Git常用命令》把架子填上):
1、补了子模块从加到删的完整链路,只留平时真会用到的部分
2、排了点卡片和表格,目录还是 toc: true 那一套
3、坑单独拎出来了,这个东西确实比普通命令更容易把自己绕进去
一些概念
一个 Git 仓库嵌在另一个 Git 仓库里。表面上是普通目录,实际上自己有独立的 .git、独立的提交历史。
父仓库只记录子模块的 path、url,以及当前钉死的那个 commit SHA。文件内容不进父仓库。
.gitmodules
父仓库根目录下的清单文件,记录每个子模块的路径、远程地址、可选的跟踪分支。这个文件是要提交的。
git ls-files --stage 里子模块目录的 mode 是 160000,它指向一次提交,不是一棵文件树。所以 git add lib 加进去的是指针,不是源码。
detached HEAD
默认update完,子模块会停在那个被钉死的 commit 上,不在分支上。要改子模块代码,先git checkout main(或你跟踪的分支),不然提交容易变成野指针。- 和普通目录的差别
克隆父仓库时如果没初始化子模块,那个目录常常是空的,看起来像丢文件,其实只是指针还没把内容拉下来。
有点子模块很适合:公共组件库、主题、文档站、第三方源码要跟着版本走。
不太建议:改得特别勤、还要和父仓库同一份 PR 一起走的业务代码。那种用 monorepo 或 subtree 更省心。
一些选项
--init第一次把.gitmodules里的配置写进本地.git/config--recursive子模块里面还有子模块时,一层层拉完--remote按跟踪分支去拉远程最新,而不是父仓库钉死的那个 SHA-b添加时指定跟踪分支,后面--remote才知道追谁-f/--force强制,路径被占用或要覆盖时才用--merge/--rebase更新子模块时选择合并策略,默认是 checkout 到指定 commit--checkout默认行为,直接把子模块工作区切到目标 commit
它长什么样
.gitmodules 大概长这样:
[submodule "lib"]
path = lib
url = https://github.com/xxx/lib.git
branch = main
branch = main不是必须的,但你后面想用update --remote,最好写上,不然它可能去追远程默认分支,跟你想的不一致。
git submodule add
把别人的仓库挂进来,最常用的就是这一条。
git submodule add <远程网址> <本地目录>
git submodule add -b main https://github.com/xxx/lib.git lib
git add .gitmodules lib
git commit -m "add lib submodule"
- 执行完会改三样东西:
.gitmodules、.git/config、以及工作区里的那个目录 - 路径已经被占用就加不进去,先挪开或换个目录名
- 添加完记得把
.gitmodules和子模块路径一起提交,少交一个,别人拉下来是空的
git submodule add <url>不写路径的话,会用仓库名当目录名
git clone(带上子模块)
普通 git clone 不会把子模块内容拉下来,目录在,货不在。
git clone --recurse-submodules <远程网址>
git clone --recurse-submodules -b <分支> <远程网址> <本地目录>
已经 clone 完才想起来?补这两步:
git submodule init
git submodule update
# 等价于:
git submodule update --init --recursive
新 Git 还可以:
git clone --recursive,和--recurse-submodules一个意思。
CI 里也要开 recursive,不然构建机上子模块目录是空的,一跑就炸。
git submodule init & update
日常同步,这两条是核心。
git submodule init
# 把 .gitmodules 登记进本地 config,还没拉代码
git submodule update
# 按父仓库记录的 SHA,把子模块 checkout 到那一次提交
git submodule update --init --recursive
# 最常用:没初始化就初始化,有嵌套就递归,一次到位
update默认是 钉死版本,不是最新 main- 父仓库
git pull之后,如果子模块指针变了,还要再update一次,否则你本地子模块还停在旧 SHA 上 - 想 checkout 时自动带着子模块走:
git config submodule.recurse true
git checkout --recurse-submodules <分支>
在子模块里干活
这个顺序一定要记住,反了同事就拉不到你的提交。
进子模块,先挂到真正的分支上,别在 detached HEAD 上直接改
改完:add / commit / push,先把子模块自己推到远程
回到父仓库,git add lib,提交这个新的 SHA 指针
再 git push 父仓库。别人 pull 父仓库 + update,才能对上你那次提交
cd lib
git checkout main
# 改代码...
git add .
git commit -m "fix: xxx"
git push
cd ..
git add lib
git commit -m "bump lib to xxx"
git push
子模块 commit 了但没 push,父仓库却先把新 SHA 推上去了。别人 update 时去远程找这个 commit,找不到,直接报错。先推子模块,再推父仓库。
git submodule update --remote
想把子模块从“钉死的旧 SHA”赶到跟踪分支的最新提交。
git submodule update --remote
git submodule update --remote --merge
git submodule update --remote lib
- 它看的是
.gitmodules里的branch,没有就用远程默认分支 - 跑完工作区变了,还要在父仓库再 commit 一次,否则只是你本地新了
- 指定跟踪分支:
git submodule set-branch -b main -- lib
git submodule set-url lib https://github.com/xxx/lib.git
更稳妥的做法其实是进子模块里自己
pull,看一眼 diff,确认没问题再回到父仓库提交指针。--remote省事,但你不一定知道它拉到了哪。
git submodule status / foreach / sync
git submodule status
# 前面带空格:和父仓库记录一致
# 前面带 + :本地子模块 SHA 和父仓库不一致(改过还没在父仓库提交,或还没 update)
# 前面带 - :还没初始化
# 后面带 (heads/main) 之类:当前在某分支上,没有则多半是 detached
git submodule foreach --recursive git status
git submodule foreach --recursive git pull
git submodule sync --recursive
# .gitmodules 里的 url 改了,用它同步到 .git/config
foreach很好用,批量给所有子模块跑同一条命令,嵌套的记得加--recursive
git rm(删除子模块)
不要只删目录,那会留下一堆残渣。
git submodule deinit -f lib
git rm -f lib
rm -rf .git/modules/lib
git commit -m "remove lib submodule"
deinit:卸本地 checkout,清.git/config里对应项git rm:从索引和.gitmodules里拿掉- 最后清
.git/modules/lib,不然以后同名路径再加回来可能很怪
老 Git 没有 deinit 的话,手工活更多:改 .gitmodules、改 .git/config、git rm --cached、再删目录。能升 Git 就升。
常见坑(真的会踩)
漏了 --recurse-submodules。补 git submodule update --init --recursive。
git pull 只更新指针,不自动把子模块内容切过去。再跑一次 update,或者打开 submodule.recurse。
提交悬在空中,一 update 就丢。先 checkout 到分支,再改再推。
别人拿到新 SHA,远程没有对应对象。顺序永远是:子模块 push → 父仓库 commit 指针 → 父仓库 push。
A 分支有这个子模块,B 分支没有,来回切会留下脏目录。切分支用 git checkout --recurse-submodules,实在乱了就 deinit 再 update --init。
git config --global submodule.recurse true,之后 checkout / pull 会尽量带着子模块一起走。不是万能,但少打很多字。
速查
| 你想做的事 | 命令 |
|---|---|
| 添加子模块 | git submodule add -b main <url> <path> |
| 克隆时一并拉下 | git clone --recurse-submodules <url> |
| 已有仓库补初始化 | git submodule update --init --recursive |
| 同步到父仓库记录的 SHA | git submodule update |
| 追跟踪分支最新 | git submodule update --remote |
| 看每个子模块状态 | git submodule status |
| 批量执行 | git submodule foreach --recursive <cmd> |
| 远程地址改了 | git submodule sync --recursive |
| 删掉 | git submodule deinit -f <path> + git rm -f <path> |
推荐工作流
不想每次都想“我现在到底在哪一层仓库”,就按这个来:
# 拉项目
git clone --recurse-submodules <url>
git pull
git submodule update --init --recursive
# 改公共库
cd lib && git checkout main && git pull
# ... 改 ...
git add . && git commit -m "xxx" && git push
cd .. && git add lib && git commit -m "bump lib" && git push
# 只想跟着父仓库走,不改子模块
git pull
git submodule update --init --recursive
记住一句话就够了:父仓库存的是指针,子模块才是代码。
改代码去子模块里改;给别人用,就把这个指针在父仓库里往前挪一格。