跳到主要内容

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 的对比​

维度submodulesubtree
克隆后是否需要额外命令需要不需要
代码在主仓库历史里否,只有指针是
日常工作流容易忘更新和普通目录一样
双向同步麻烦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 拆包时非常好用,且不会丢掉该目录的历史。