一个管「引」,一个管「抄」:Submodule 和 Subtree 到底差在哪

上篇写了单仓和 Submodule:自己团队的代码往一个仓库里收,要挂别人维护的独立仓库才用 Submodule。发完就有人来问我:那 Git Subtree 呢?

他是看到网上不少文章说 subtree 是 submodule 的「替代品」,正纠结要不要迁过去。我跟他讲,这俩不是谁替代谁,是两条相反的路。Submodule 是「引」:把别人的仓库挂进来,代码还留在外面,主仓只记一个引用;Subtree 是「抄」:把外部仓库的代码整个抄进你的仓库,抄完就是普通文件,但留着一条跟上游同步的路。一个是请租客住进来,一个是把东西搬进自己家,怎么替代?

先弄清它俩分别是啥

Git Submodule:主仓库只记两样:子模块的远程地址,和一个固定的 commit 哈希(gitlink 引用)。子模块目录拉下来是完整的独立仓库,有自己的历史、分支、标签,代码并不真的躺在主仓里,真身还在它自己的仓库。

Git Subtree :一条命令把上游仓库的提交历史接进当前仓库的某个子目录(命令下一节统一给)。接进来之后目录就是普通目录:文件在主仓历史里有真实的提交记录,clone 一步到位,日常改完直接 add/commit,没有第二套版本体系。主仓和上游之间靠一次特殊合并提交维系,提交信息里带着 git-subtree-dir: libs/util 标记。

还得先澄清一件事:git subtree 不是 Git 亲儿子。它是 Avery Pennarun 2009 年写的脚本,后来被收进 Git 官方源码树的 contrib/subtree/ 目录跟着一起分发。Git for Windows、Homebrew、Ubuntu 这些常见环境都把它捆绑好了,所以多数人没意识到它不是原生功能;个别精简环境(Alpine 的 Docker 镜像、源码编译的 Git)没带它,会报 git: 'subtree' is not a git command,装个 git-subtree 包就行。而 git submodule 才是 Git 核心自带的能力,不存在「没捆绑」这回事。

关键差异,一条条过

克隆

  • Submodule:clone 完主仓还得再拉一次子模块(git submodule update --init),这一步忘了,子目录就是空的。
  • Subtree:一次 git clone 全部到手,子目录天然有内容。

日常改动

  • Submodule:两步。先进子模块提交推送,再回主仓更新引用,漏一步就乱。
  • Subtree:目录里的文件就是普通文件,改完直接 add/commit,无感。

和上游的来往

  • Submodule:拉上游更新只是挪一下指针(git submodule update),代码不进主仓;本地改完想回给上游,直接在子仓库里 push,天生支持。
  • Subtree:拉上游要把对方的新提交并入本地(git subtree pull);本地改动想回推给上游最绕,得先 git subtree split 把目录历史拆成独立分支再推,容易翻车。

历史与体积

  • Submodule:主仓永远很小,历史都留在各子仓里。
  • Subtree:上游历史跟着并进主仓,不带 --squash,主仓会被迅速撑大。

权限

  • Submodule:子模块是独立仓库,权限在各自的 Git 服务端设,能按仓库管。
  • Subtree:跟着主仓走,只有仓库级权限,管不到某个目录。

绕回来看,两边差别都来自同一个取舍:Submodule 保「独立」,宁可操作繁琐也要让子仓库能各自演进;Subtree 求「融入」,宁可历史混进来也要让目录跟主仓自己长的一样。跨仓库的联动改动两边都不顺手,这是各自的选择直接换来的。

Subtree 日常就三条命令

比 Submodule 多的,是一个 push 方向。核心都在 git subtree 下面:

首次挂入:

css 复制代码
git subtree add --prefix=libs/util <地址> main --squash

上游更新了,拉下来合并:

css 复制代码
git subtree pull --prefix=libs/util <地址> main --squash

本地改完想回给上游:

css 复制代码
git subtree push --prefix=libs/util <地址> main

--squash 建议始终带上:它把上游的历史压成主仓里的一个提交。不带也行,但上游从创始以来的每一条 commit 都会混进你的主仓历史,仓库体积和 log 都被撑大,回推时也没多大帮助。

什么时候用哪个

Submodule 适合:主项目要集成外部独立维护的仓库,权责清晰、基本只拉不改。物理隔离是第一诉求,宁可两步操作也认。它回推本地改动最简单,直接在子仓库 push,这是 subtree 给不了的。

Subtree 适合:想把第三方或兄弟团队的代码抄进来自己改着用,还想偶尔跟上上游。典型场景是「fork 上游自己魔改,又不想彻底断奶」:改完自己的,隔段时间 pull 一次跟上游同步;本地改得差不多了,split 拆出分支回推给上游。团队不想学 Submodule 那套两步操作的,subtree 无感得像普通目录,上手成本低。

自己团队天天一起改的代码,还是回单仓,理由上篇说过,不展开。

这些坑我都踩过

第一个坑是历史膨胀。我头一回用 subtree 拉一个上游库进来,忘了带 --squash,几 MB 的库带进来几万条 commit,主仓 log 一翻全是别人的提交,克隆也变慢了。从那次起我每次 add 都条件反射带上 --squash

第二个坑在回推。subtree 靠提交信息里的 git-subtree-dir 标记反推历史,本地一旦对这些提交做过 rebase、amend、filter 之类的改写,标记就丢了,split 出来的历史会缺料甚至推不动。现在我要回推,都先 git subtree split --prefix=libs/util -b 临时分支 检查一遍,再推这个分支。

第三个坑是两头同时改。本地改了 util,上游也改了 util,pull 时冲突得手工解;解完想再回推,冲突的合并提交又让 split 更难还原。subtree 适合「一头动得多、另一头动得少」的单向场景,两头高频对改,迟早得拆成独立仓库。

第四个坑,别把它当同步银弹。subtree 的本质是「抄一份进来 + 有方向地同步」,不是两个仓库间的实时双向镜像。真要两个团队并行高频协作一个组件,老老实实独立仓库 + 版本发布,比任何嵌法都省心。

最容易弄混的一处

「Subtree 里的目录」和普通目录长得一模一样,clone 完都是普通文件夹。这是它好用之处,也是最容易埋雷的地方:新同事根本看不出 libs/util 是从外部同步来的,直接在目录里自由改动,改完也没人记得回推。Submodule 目录至少有 .gitmodules 和独立的仓库状态可查,Subtree 的「出身」藏在提交历史里,肉眼不可见。项目里用了 Subtree,必须在 README 里写清楚:哪些目录是从哪同步来的、同步命令是什么。

三个方案各干各的活,别老想着让一个干掉另一个。下次再纠结,先问自己一句:这代码是你自己团队在写,还是要跟外部仓库打交道?前者收进单仓;后者再分------只想用、基本不改的用 submodule,想抄进来自己魔改又不断奶的用 subtree。你手里是哪一种?


参考来源

  • Atlassian Git 教程《Git subtree:Git 子模块的替代方案》(subtree add/pull/push 基本用法):www.atlassian.com/zh/git/tuto...
  • Git 官方源码 contrib/subtree/git-subtree.sh(脚本作者 Avery Pennarun,版权 2009;subtree 随 Git 官方源码树一起维护):github.com/git/git/blo...
  • Pro Git《Git 工具------高级合并》(子树合并小节讲的是底层的 subtree merge 策略,git subtree 命令内部用的就是它,两者不是一回事):git-scm.com/book/zh/v2/...
  • Git 官方文档《Git 工具------子模块》(对照阅读 submodule 的官方标准用法):git-scm.com/book/zh/v2/...
  • Stack Overflow「Differences between git submodule and subtree」(社区对两者差异的高票总结):stackoverflow.com/questions/3...
相关推荐
lingran__2 小时前
Git 完全指南(三):远程仓库与标签管理
开发语言·git·gitee·ssh·团队协作·远程仓库·分布式版本控制
lingran__17 小时前
Git 完全指南(二):分支管理
git·分支管理·版本控制·团队协作·多人开发·git flow
必须会一定会19 小时前
Agent Handoff v0.6.0 跨电脑同步:Git、EVENTS.jsonl、CONTEXT.md 使用方法
人工智能·git·ai编程
cui_hao_nan1 天前
Git常用命令1
运维·git
xxwl5851 天前
Git常用命令的学习
git·学习
攻城狮-申1 天前
git本地分支对齐远程分支
前端·git
不怕犯错,就怕不做1 天前
git prune 自动删除本地记录中那些远程已经不存在的分支引用
linux·服务器·git
码云之上2 天前
换模型之后,Chatbot 为什么要自己做 compact?
前端·agent·前端工程化
ELI_He9992 天前
如何还原已经推送的提交
git