Skip to content

子树

概述

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 pullsubmodule update
历史管理可选择压缩独立历史
推送修改subtree push在子模块中推送
磁盘占用较大较小

子树最佳实践

1. 使用 --squash

bash
# 推荐使用 --squash 保持历史简洁
git subtree add --prefix=libs/library library main --squash
git subtree pull --prefix=libs/library library main --squash

2. 使用远程别名

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 --squash

3. 文档化子树

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 --squash

4. 定期更新

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 保持历史简洁
  • 使用远程别名简化命令
  • 文档化子树信息
  • 定期检查并更新子树