Git Subtree:把另一个仓库嵌进来
上一类需求(仓库里引另一个仓库的代码)submodule 能解决,但要求每个人记得 --recurse-submodules,而且代码不在主仓库的提交图里。
subtree 的思路相反:把子仓库的提交真正合进主仓库的历史,目录就是普通目录。克隆的人什么都不用额外做。
一、添加
git subtree add --prefix=libs/utils https://xxx/utils.git main --squash
--prefix:放进哪个目录,必须选一个还不存在的路径--squash:把子仓库历史压成一条记录,主仓库历史更干净(推荐)
不加 --squash 会把子仓库全部历史搬进来,仓库体积变大但以后 log 能追到每条原始提交。
二、拉取更新
git subtree pull --prefix=libs/utils https://xxx/utils.git main --squash
每次都要写全 URL 和分支,很啰嗦。加个别名配置:
git remote add utils https://xxx/utils.git
git subtree pull --prefix=libs/utils utils main --squash
三、把改动推回子仓库
git subtree push --prefix=libs/utils utils main
这条命令是实时计算分支的——在主仓库里改的子模块代码,可以原样推回上游仓库。
⚠️ 但要注意:如果主仓库的历史里这个前缀被改过多次、有复杂合并,subtree push 计算出的分支会很慢甚至冲突。用 --squash 模式能缓解不少。
四、和 submodule 的对比
| 维度 | submodule | subtree |
|---|---|---|
| 克隆后是否需要额外命令 | 需要 | 不需要 |
| 代码在主仓库历史里 | 否,只有指针 | 是 |
| 日常工作流 | 容易忘更新 | 和普通目录一样 |
| 双向同步 | 麻烦 | pull / push 都支持 |
| 仓库体积 | 小 | 大(历史被搬进来) |
| 适用 | 版本精确锁定、独立演进 | 代码要被频繁修改 |
一句话:能接受仓库大一点、图省心,选 subtree;要精确锁版本、子仓库很重,选 submodule。
五、常见坑
--prefix目录必须已存在或由命令创建,且不能已纳入版本管理subtree pull产生合并提交,冲突处理和普通 merge 一样subtree push不会自动处理"上游已经前进"的情况,先pull再push- 用
--squash之后,主仓库里看不到子仓库的原始提交,追责时要去上游仓库看
六、一个实用变体:拆分仓库
subtree 反向用就是拆分——把某个目录的历史抽成独立仓库:
git subtree split --prefix=libs/utils -b utils-only
git push https://xxx/utils.git utils-only:main
这在 monorepo 拆包时非常好用,且不会丢掉该目录的历史。