Appearance
子树
概述
Git 子树(Subtree)是管理外部依赖的另一种方式,它将外部仓库的历史合并到主仓库的子目录中。与子模块不同,子树将外部代码直接包含在主仓库中。
什么是子树
子树是一种将外部仓库合并到主项目子目录的方法:
- 外部代码直接存储在主仓库中
- 保留外部仓库的提交历史
- 不需要额外的配置文件
- 操作相对简单
子树的特点
- 代码内嵌:外部代码直接包含在主仓库
- 历史保留:保留外部仓库的提交历史
- 无配置文件:不需要 .gitmodules 文件
- 离线工作:不依赖外部仓库的连接
添加子树
基本添加
bash
# 添加子树
git subtree add --prefix=<path> <repository-url> <branch> --squash
# 示例:添加第三方库
git subtree add --prefix=libs/library https://github.com/user/library.git main --squash
# 不使用 --squash(保留完整历史)
git subtree add --prefix=libs/library https://github.com/user/library.git main参数说明
| 参数 | 说明 |
|---|---|
--prefix | 子树存放的路径 |
--squash | 压缩历史为单个提交 |
<repository-url> | 远程仓库地址 |
<branch> | 要添加的分支 |
添加远程仓库别名
bash
# 添加远程仓库别名,方便后续操作
git remote add library https://github.com/user/library.git
# 使用别名添加子树
git subtree add --prefix=libs/library library main --squash添加后的目录结构
bash
project/
├── libs/
│ └── library/ # 子树目录
│ ├── src/
│ ├── README.md
│ └── ...
├── src/
└── README.md更新子树
从上游拉取更新
bash
# 更新子树
git subtree pull --prefix=libs/library library main --squash
# 不压缩历史
git subtree pull --prefix=libs/library library main
# 使用远程 URL
git subtree pull --prefix=libs/library https://github.com/user/library.git main --squash更新流程
bash
# 1. 获取远程更新
git fetch library
# 2. 拉取子树更新
git subtree pull --prefix=libs/library library main --squash
# 3. 解决可能的冲突
# 4. 推送主项目
git push origin main子树操作
推送修改到上游
bash
# 如果你在子树目录中做了修改,可以推送回原仓库
git subtree push --prefix=libs/library library main
# 推送到特定分支
git subtree push --prefix=libs/library library feature-branch合并子树更新
bash
# 合并特定的上游提交
git subtree merge --prefix=libs/library library/main
# 使用特定提交
git subtree merge --prefix=libs/library <commit-hash>分离子树
bash
# 将子树拆分为独立仓库
git subtree split --prefix=libs/library -b library-branch
# 创建新仓库
mkdir ../new-library-repo
cd ../new-library-repo
git init
git pull ../main-repo library-branch子树工作流程
场景一:添加第三方库
bash
# 1. 添加远程仓库
git remote add vue-router https://github.com/vuejs/vue-router.git
# 2. 添加子树
git subtree add --prefix=libs/vue-router vue-router main --squash
# 3. 定期更新
git subtree pull --prefix=libs/vue-router vue-router main --squash场景二:共享代码库
bash
# 主项目 A 添加共享库
git subtree add --prefix=shared https://github.com/company/shared-lib.git main --squash
# 主项目 B 也添加相同的共享库
git subtree add --prefix=shared https://github.com/company/shared-lib.git main --squash
# 在项目 A 中修改共享库
cd shared
vim utils.js
git add shared
git commit -m "更新共享库"
# 推送到共享库仓库
git subtree push --prefix=shared https://github.com/company/shared-lib.git main
# 在项目 B 中拉取更新
git subtree pull --prefix=shared https://github.com/company/shared-lib.git main --squash场景三:版本锁定
bash
# 添加特定标签版本
git subtree add --prefix=libs/library https://github.com/user/library.git v1.2.0 --squash
# 更新到新版本
git subtree pull --prefix=libs/library https://github.com/user/library.git v1.3.0 --squash子树高级用法
使用 --squash vs 不使用
bash
# 使用 --squash(推荐)
# 优点:历史简洁,只有一个合并提交
# 缺点:丢失原始提交历史
git subtree add --prefix=libs/library library main --squash
# 不使用 --squash
# 优点:保留完整历史
# 缺点:历史复杂,提交数量多
git subtree add --prefix=libs/library library main查看子树历史
bash
# 查看子树的提交历史
git log --oneline --grep="library" -- libs/library
# 查看子树相关的合并提交
git log --oneline --merges -- libs/library子树配置
bash
# 查看子树配置
git config -l | grep subtree
# 子树信息存储在 .git/config 中
# [subtree "libs/library"]
# url = https://github.com/user/library.git子树脚本封装
添加子树脚本
bash
#!/bin/bash
# add-subtree.sh
NAME=$1
URL=$2
PATH=$3
BRANCH=${4:-main}
git remote add $NAME $URL
git subtree add --prefix=$PATH $NAME $BRANCH --squash
echo "子树 $NAME 已添加到 $PATH"更新所有子树脚本
bash
#!/bin/bash
# update-subtrees.sh
# 定义子树列表
declare -A SUBTREES=(
["library"]="https://github.com/user/library.git:libs/library:main"
["utils"]="https://github.com/user/utils.git:libs/utils:main"
)
for name in "${!SUBTREES[@]}"; do
IFS=':' read -r url path branch <<< "${SUBTREES[$name]}"
echo "更新子树: $name"
git subtree pull --prefix=$path $url $branch --squash
done
echo "所有子树已更新"子树常见问题
问题一:合并冲突
bash
# 子树更新时发生冲突
git subtree pull --prefix=libs/library library main --squash
# 解决冲突
# 1. 查看冲突文件
git status
# 2. 手动解决冲突
# 编辑冲突文件...
# 3. 标记为已解决
git add .
# 4. 完成合并
git commit问题二:推送失败
bash
# 如果推送时出现历史不匹配
git subtree push --prefix=libs/library library main
# 可能需要先 split
git subtree split --prefix=libs/library --annotate="(library)" -b library-sync
git push library library-sync:main问题三:历史过于复杂
bash
# 如果不使用 --squash 导致历史复杂
# 可以重新添加子树
# 1. 删除子树目录
git rm -rf libs/library
git commit -m "移除子树"
# 2. 重新添加(使用 --squash)
git subtree add --prefix=libs/library library main --squash子树 vs 子模块对比
| 特性 | 子树 | 子模块 |
|---|---|---|
| 代码存储 | 直接在主仓库 | 独立仓库引用 |
| 配置文件 | 无需 | 需要 .gitmodules |
| 克隆复杂度 | 简单 | 需要初始化 |
| 更新方式 | subtree pull | submodule update |
| 历史管理 | 可选择压缩 | 独立历史 |
| 推送修改 | subtree push | 在子模块中推送 |
| 磁盘占用 | 较大 | 较小 |
子树最佳实践
1. 使用 --squash
bash
# 推荐使用 --squash 保持历史简洁
git subtree add --prefix=libs/library library main --squash
git subtree pull --prefix=libs/library library main --squash2. 使用远程别名
bash
# 使用别名简化命令
git remote add library https://github.com/user/library.git
git subtree add --prefix=libs/library library main --squash
git subtree pull --prefix=libs/library library main --squash3. 文档化子树
markdown
## 子树依赖
本项目使用以下子树:
| 名称 | 路径 | 版本 | 来源 |
|------|------|------|------|
| library | libs/library | v1.2.0 | https://github.com/user/library |
| utils | libs/utils | v2.0.0 | https://github.com/user/utils |
更新命令:
git subtree pull --prefix=libs/library library main --squash4. 定期更新
bash
# 设置定期更新计划
# crontab 或 CI/CD 中定期检查更新
# 检查是否有更新
git fetch library
git log HEAD..library/main --oneline总结
| 操作 | 命令 | 说明 |
|---|---|---|
| 添加 | git subtree add --prefix=<path> <url> <branch> --squash | 添加子树 |
| 拉取 | git subtree pull --prefix=<path> <remote> <branch> --squash | 拉取更新 |
| 推送 | git subtree push --prefix=<path> <remote> <branch> | 推送修改 |
| 拆分 | git subtree split --prefix=<path> -b <branch> | 分离子树 |
| 合并 | git subtree merge --prefix=<path> <commit> | 合并更新 |
使用建议:
- 使用
--squash保持历史简洁 - 使用远程别名简化命令
- 文档化子树信息
- 定期检查并更新子树
