Docs Library
Git Subtree 使用指南
解释 git subtree 的概念、与 submodule 的对比、添加和更新子树的操作,以及子树合并和拆分的高级用法。
- 想先理解历史图再看命令的人
- 知道提交不是文件快照列表那么简单
- 把概念页当命令说明页使用
一句话理解
git subtree 允许你将一个仓库作为子目录嵌入到另一个仓库中,子树的内容会成为主仓库历史的一部分,而不是像 submodule 那样作为外部引用。
Subtree 与 Submodule 的对比
这是理解 git subtree 的关键起点。两种方式都能实现"一个仓库包含另一个仓库",但实现机制截然不同:
| 特性 | git subtree | git submodule |
|---|---|---|
| 存储方式 | 子仓库文件直接存在于主仓库中 | 子仓库是独立仓库,主仓库只存引用(commit hash) |
| clone 后 | 所有文件立即可用,无需额外操作 | 需要 git submodule update --init 初始化 |
| 历史记录 | 子树的历史会混入主仓库历史 | 子仓库历史独立,主仓库只记录指向的 commit |
| 修改子仓库 | 可以直接在主仓库中修改,然后 push 回子仓库远端 | 需要在子目录中单独操作 |
| 仓库大小 | 主仓库包含子仓库所有文件,体积较大 | 主仓库体积小,子仓库独立 |
| 复杂度 | 操作简单,没有 submodule 的那些坑 | 需要额外学习 submodule 的专用命令 |
| 共享使用 | 其他项目不能直接引用你的 subtree 版本 | 多个项目可以引用同一个 submodule 的同一个版本 |
何时选择 subtree
- 你希望团队成员 clone 后立刻能用,不需要额外步骤
- 子项目不大,不会对主仓库体积造成明显影响
- 你经常需要修改子项目代码
- 团队不熟悉或不喜欢 submodule 的复杂度
何时选择 submodule
- 子项目很大(如大型框架、引擎)
- 你需要精确控制子项目的版本
- 多个独立项目共享同一个子项目
- 子项目有独立的开发团队和发布周期
lodash.git (工具库)docs-site.git (文档站)shared-lib.git (共享库)
add → 子仓库历史嵌入主仓库pull → 拉取子仓库更新到子目录push → 将子目录修改推送回子仓库
--squash 参数会将子仓库的多次提交合并为一次,保持主仓库历史简洁。
基本操作
添加子树
# 语法
git subtree add --prefix=<目录> <仓库URL> <分支> [--squash]
# 示例:将 lodash 库添加到 vendor/lodash 目录
git subtree add --prefix=vendor/lodash https://github.com/lodash/lodash.git main --squash
参数说明:
--prefix:子树在主仓库中的存放目录<仓库URL>:子仓库的远程地址<分支>:要引入的子仓库分支--squash:将子仓库的所有提交压缩为一个提交(推荐,避免引入大量历史)
查看子树
# 列出子树目录中的文件
ls vendor/lodash/
# 查看子树相关的提交
git log --oneline -- vendor/lodash/
更新子树(拉取上游变更)
# 语法
git subtree pull --prefix=<目录> <仓库URL> <分支> [--squash]
# 示例:拉取 lodash 的最新变更
git subtree pull --prefix=vendor/lodash https://github.com/lodash/lodash.git main --squash
--squash 同样推荐,避免每次更新都引入完整历史。
推送子树变更(向子仓库上游推送)
# 语法
git subtree push --prefix=<目录> <仓库URL> <分支>
# 示例:将你对 lodash 的修改推回上游
git subtree push --prefix=vendor/lodash https://github.com/lodash/lodash.git my-fix-branch
注意:这要求你对子仓库有写入权限,通常用于你 fork 的子仓库。
子树拆分(Split)
git subtree split 是 subtree 最强大的功能之一。它能把主仓库中某个子目录的历史拆分成一个独立的分支,这个分支可以直接作为独立仓库推送。
拆分场景
当你决定将子目录提升为独立仓库时:
# 将 vendor/lodash 目录拆分为独立分支
git subtree split --prefix=vendor/lodash --branch=lodash-standalone
这会创建一个新分支 lodash-standalone,其中只包含 vendor/lodash/ 目录的文件和历史(路径已调整为根目录)。
推送到新仓库
# 拆分并推送到新远端
git subtree split --prefix=vendor/lodash --branch=lodash-standalone
# 添加新远端
git remote add lodash-origin https://github.com/yourname/lodash.git
# 推送拆分后的分支
git push lodash-origin lodash-standalone:main
重复拆分和推送
当你后续在主仓库中对子目录做了修改,可以再次拆分并推送:
# 再次拆分(Git 会记住上次拆分的起点)
git subtree split --prefix=vendor/lodash --branch=lodash-standalone
# 推送新变更
git push lodash-origin lodash-standalone:main
高级用法
不使用 --squash 添加子树
如果你需要保留子仓库的完整历史:
# 不带 --squash:保留完整历史
git subtree add --prefix=vendor/lodash https://github.com/lodash/lodash.git main
# 这会引入 lodash 的所有提交历史
git log --oneline | head -20
从已有的子目录创建子树
如果你已经在主仓库中有了一个目录,想把它关联到一个远端仓库:
# 先添加远端
git remote add -f lodash https://github.com/lodash/lodash.git
# 然后添加子树(使用已有目录)
git subtree add --prefix=vendor/lodash lodash main --squash
在子树中工作并提交
# 在子目录中直接修改
cd vendor/lodash
# 编辑文件...
# 在主仓库根目录提交
cd ../..
git add vendor/lodash/
git commit -m "fix: 修复 lodash 中的某个问题"
# 推送到子仓库远端
git subtree push --prefix=vendor/lodash https://github.com/yourname/lodash.git fix-branch
查看子树的提交差异
# 查看子树目录的变更
git log --oneline -p -- vendor/lodash/
# 比较两次提交间子树的变化
git diff HEAD~5..HEAD -- vendor/lodash/
工作流示例
场景:前端项目引入公共组件库
# 1. 添加公共组件库作为子树
git subtree add --prefix=libs/common-ui https://github.com/company/common-ui.git main --squash
# 2. 在日常开发中直接修改组件
git add libs/common-ui/
git commit -m "feat: 为 CommonButton 添加 loading 状态"
# 3. 定期同步上游更新
git subtree pull --prefix=libs/common-ui https://github.com/company/common-ui.git main --squash
# 4. 将修改贡献回上游
git subtree push --prefix=libs/common-ui https://github.com/yourname/common-ui.git feature/loading
场景:从单体仓库拆分微服务
# 1. 从单体仓库中拆分出 auth-service
git subtree split --prefix=services/auth --branch=auth-service-split
# 2. 创建新仓库并推送
cd /tmp
git init auth-service
cd auth-service
git pull /path/to/monorepo auth-service-split
# 3. 添加远端并推送
git remote add origin https://github.com/company/auth-service.git
git push -u origin main
# 4. 在单体仓库中保留子树关系
git subtree add --prefix=services/auth https://github.com/company/auth-service.git main --squash
常见问题
1. subtree pull 冲突
# 如果 pull 时发生冲突,手动解决后继续
git subtree pull --prefix=vendor/lib https://example.com/lib.git main --squash
# 如果有冲突,正常解决:
# 编辑冲突文件 → git add → git commit
# 注意:不要使用 git merge --abort,subtree 的 merge 需要特殊处理
2. 找不到上次拆分的 commit
# Git 通过 commit message 中的 "git-subtree-dir" 来跟踪
# 如果丢失,可以手动指定:
git subtree split --prefix=vendor/lodash --branch=lodash-split --rejoin
3. subtree push 失败
# push 失败通常是因为远端有新提交
# 先 pull 再 push
git subtree pull --prefix=vendor/lib https://example.com/lib.git main --squash
git subtree push --prefix=vendor/lib https://example.com/lib.git main
4. 子树太大影响性能
如果子树体积过大,考虑:
- 使用
--squash减少历史大小 - 切换到
git submodule - 使用
git sparse-checkout减少检出范围
调试技巧
查看子树相关的提交
# 搜索包含 "Subtree" 的提交信息
git log --grep="Subtree" --oneline
# 查看特定子树目录的所有变更
git log --oneline --follow -- vendor/lodash/
检查子树是否正确引入
# 确认子树目录存在
ls -la vendor/lodash/
# 确认子树目录被正确跟踪
git ls-tree HEAD vendor/lodash/ | head -5
注意事项
--squashvs 完整历史:团队项目推荐--squash,保持主仓库历史清爽- 子树 push 需要写入权限:你只能推送到有权限的子仓库
- 子树目录路径变更:如果重命名了子树目录,后续操作需要使用新路径
- 合并冲突:子树的 merge commit 信息较长,冲突时需要仔细阅读
- 备份:对重要的子树操作,建议先创建备份分支
继续学习建议
git submodule—— 对比理解两种子项目管理方式git sparse-checkout—— 部分检出大仓库monorepo工具(Turborepo、Nx、pnpm workspace)