搜索 K
主题
git submodule 和 git subtree 是 Git 中管理子项目依赖的两种方案。它们解决同一个问题:如何在一个仓库中引用另一个仓库的代码,但设计理念和使用方式截然不同。
submodule 是链接(记录指向另一个仓库的指针),subtree 是复制(将另一个仓库的代码合并进来)。
| 对比项 | submodule | subtree |
|---|---|---|
| 数据存储 | 只存引用(SHA),代码在独立仓库 | 代码直接合并到主仓库 |
| 克隆体验 | 需要 --recurse-submodules | 普通克隆即可获得全部代码 |
| 更新方式 | git submodule update | git subtree pull |
| 贡献回推 | 在子模块目录中独立 push | git subtree push |
| 学习曲线 | 较陡(概念多、易出错) | 较平缓 |
| 历史记录 | 子项目历史独立 | 子项目历史混入主仓库 |
.gitmodules 文件 | 需要 | 不需要 |
| 适用场景 | 大型子项目、需要独立版本控制 | 小型库、vendor 代码 |
submodule 让你在一个 Git 仓库中嵌入另一个 Git 仓库。主仓库只记录子模块的仓库 URL 和特定提交 SHA,不直接包含子模块的代码文件。
# 添加 submodule 后,主仓库会多出两个东西:
# 1. .gitmodules 文件 — 记录子模块的配置
cat .gitmodules
# [submodule "libs/shared"]
# path = libs/shared
# url = https://github.com/team/shared-lib.git
# 2. 特殊的树对象 — 记录子模块锁定的提交 SHA
git ls-tree HEAD libs/shared
# 160000 commit abc1234... libs/shared
# 160000 = 子模块类型# 添加子模块
git submodule add https://github.com/team/shared-lib.git libs/shared
# 指定分支(跟踪远程分支而非固定 SHA)
git submodule add -b main https://github.com/team/shared-lib.git libs/shared
# 执行后发生了什么:
# 1. 克隆子仓库到 libs/shared/
# 2. 创建 .gitmodules 文件
# 3. 在暂存区记录子模块的 SHA
# 4. 需要提交这些变更
git commit -m "chore: 添加 shared-lib 子模块"# 方式 1:克隆时一起初始化子模块(推荐)
git clone --recurse-submodules https://github.com/team/main-project.git
# 方式 2:先克隆再初始化
git clone https://github.com/team/main-project.git
cd main-project
git submodule init # 注册子模块
git submodule update # 检出子模块到记录的 SHA
# 简写
git submodule update --init
# 递归初始化(子模块中还有子模块)
git submodule update --init --recursive如果直接 git clone 而不加 --recurse-submodules,子模块目录会是空的!这是很多新手困惑的来源。
# 当你切换分支、pull 更新后,子模块可能需要同步
git submodule update
# 递归更新
git submodule update --recursive
# 常见场景:pull 后子模块指针变了
git pull
# 提示 libs/shared 有变更
git submodule update# 将子模块更新到远程的最新提交
git submodule update --remote
# 默认跟踪 master/main 分支
# 更新特定子模块
git submodule update --remote libs/shared
# 更新后需要提交主仓库的引用变更
git add libs/shared
git commit -m "chore: 更新 shared-lib 到最新版本"# 在 .gitmodules 中设置跟踪分支
git config -f .gitmodules submodule.libs/shared.branch develop
# 或直接编辑 .gitmodules
[submodule "libs/shared"]
path = libs/shared
url = https://github.com/team/shared-lib.git
branch = develop# 进入子模块目录
cd libs/shared
# 子模块默认处于 detached HEAD 状态!
git status
# HEAD detached at abc1234
# 如果要在子模块中做修改,先切换到分支
git switch main
# 做修改
git commit -am "feat: 添加新功能"
git push origin main
# 回到主仓库,更新子模块引用
cd ../..
git add libs/shared
git commit -m "chore: 更新 shared-lib 引用"
git push直接在子模块中提交而不先切换到分支,提交会成为悬空提交,可能被垃圾回收丢失!
# ❌ 在 detached HEAD 上提交
cd libs/shared
git commit -am "fix" # 悬空提交!
# ✅ 先切换到分支
cd libs/shared
git switch main
git commit -am "fix"
git push删除子模块比添加复杂,需要清理多处:
# 步骤 1:取消注册子模块
git submodule deinit -f libs/shared
# 步骤 2:从暂存区和工作目录删除
git rm -f libs/shared
# 步骤 3:删除 .git/modules 中的缓存
rm -rf .git/modules/libs/shared
# 步骤 4:提交
git commit -m "chore: 移除 shared-lib 子模块"git rm libs/shared
rm -rf .git/modules/libs/shared
git commit -m "chore: 移除 shared-lib 子模块"# 查看所有子模块状态
git submodule status
# abc1234 libs/shared (v1.0.0) 正常
# +def5678 libs/utils (v1.0.0-1-gdef5678) 有新提交未记录
# -ghi9012 libs/tools 未初始化
# 对所有子模块执行命令
git submodule foreach 'git pull origin main'
git submodule foreach 'git status'
git submodule foreach --recursive 'git clean -fd'
# 查看子模块的 diff
git diff --submodule
# 显示子模块中变更的提交列表
# 配置 diff 默认显示子模块详情
git config --global diff.submodule log# 多个项目共享一个内部工具库
# project-a/
# libs/shared-utils/ ← 子模块
# project-b/
# libs/shared-utils/ ← 同一个子模块
# 在 project-a 中更新子模块
cd project-a/libs/shared-utils
git switch main
git pull origin main
# 做修改并推送
git commit -am "feat: 新工具函数"
git push
# 在 project-b 中同步更新
cd project-b
git submodule update --remote libs/shared-utils
git add libs/shared-utils
git commit -m "chore: 同步 shared-utils 更新"# 将第三方库锁定到特定版本
git submodule add https://github.com/vendor/lib.git vendor/lib
cd vendor/lib
git checkout v2.3.1 # 锁定到 v2.3.1
cd ../..
git add vendor/lib
git commit -m "chore: 添加 vendor/lib v2.3.1"
# 升级到新版本
cd vendor/lib
git fetch --tags
git checkout v2.4.0
cd ../..
git add vendor/lib
git commit -m "chore: 升级 vendor/lib 到 v2.4.0"# 微服务架构中,某些服务需要共享 proto 定义
# main-project/
# proto/ ← 子模块(共享的 protobuf 定义)
# service-a/
# service-b/
# 配置 CI 自动更新子模块
# .github/workflows/ci.yml
# steps:
# - uses: actions/checkout@v4
# with:
# submodules: recursivesubtree 将另一个仓库的代码直接合并到主仓库的某个子目录中。代码成为主仓库的一部分,不需要额外的初始化步骤。
# 添加远程仓库(可选但推荐,方便后续操作)
git remote add shared-lib https://github.com/team/shared-lib.git
# 将远程仓库的代码合并到子目录
git subtree add --prefix=libs/shared shared-lib main --squash
# --prefix 子目录路径
# shared-lib 远程仓库名
# main 远程分支
# --squash 将子仓库的历史压缩为一个提交(推荐)
# 不使用 --squash(保留完整历史)
git subtree add --prefix=libs/shared shared-lib main--squash 的选择--squash(推荐):子仓库的所有历史压缩为一个提交,主仓库历史更干净--squash:保留子仓库的完整提交历史,可以看到每次修改的细节如果你选择了 --squash,后续的 pull 和 push 也必须始终使用 --squash。
# 从远程拉取子仓库的最新代码
git subtree pull --prefix=libs/shared shared-lib main --squash
# 不使用远程别名时
git subtree pull --prefix=libs/shared https://github.com/team/shared-lib.git main --squash# 如果你在主仓库中修改了 subtree 目录下的代码,可以推送回原始仓库
git subtree push --prefix=libs/shared shared-lib main
# 使用 --squash 推送
git subtree push --prefix=libs/shared shared-lib feature/update --squashsubtree push 会遍历主仓库的提交历史,找出所有影响了 --prefix 目录的提交,将它们重新组织为只包含子目录修改的提交,然后推送到子仓库。
# 将子目录的提交历史拆分为一个独立的分支
git subtree split --prefix=libs/shared -b shared-branch
# 然后可以推送到独立仓库
git push shared-lib shared-branch:main# 将第三方库的代码直接纳入项目
git remote add lodash https://github.com/lodash/lodash.git
git subtree add --prefix=vendor/lodash lodash main --squash
# 这样即使第三方仓库不可用,你的项目也能正常构建
# 更新时
git subtree pull --prefix=vendor/lodash lodash main --squash# 从 monorepo 中将某个目录拆分为独立仓库
# monorepo/
# packages/
# shared-utils/ ← 要拆分的部分
# app/
# api/
# 1. 拆分历史到新分支
git subtree split --prefix=packages/shared-utils -b split-shared-utils
# 2. 创建新仓库
mkdir ../shared-utils
cd ../shared-utils
git init
git pull ../monorepo split-shared-utils
# 3. 设置远程并推送
git remote add origin https://github.com/team/shared-utils.git
git push -u origin main
# 4. 在原 monorepo 中用 subtree 引用新仓库
cd ../monorepo
git rm -r packages/shared-utils
git subtree add --prefix=packages/shared-utils https://github.com/team/shared-utils.git main --squash# 多个项目共享 ESLint、Prettier 等配置
git remote add configs https://github.com/team/shared-configs.git
git subtree add --prefix=config/shared configs main --squash
# 项目结构
# project/
# config/
# shared/ ← subtree(共享配置)
# .eslintrc.json
# .prettierrc
# local/ ← 项目特有配置
# tsconfig.json
# 配置更新后同步
git subtree pull --prefix=config/shared configs main --squash# === submodule 的团队体验 ===
# 新成员克隆项目
git clone https://github.com/team/project.git
cd project
# 子模块目录是空的!
ls libs/shared/ # 什么都没有
# 需要额外步骤
git submodule update --init --recursive
# 不熟悉的人容易忘记这一步
# === subtree 的团队体验 ===
# 新成员克隆项目
git clone https://github.com/team/project.git
cd project
# 所有代码都在!
ls libs/shared/ # 所有文件都在
# 无需额外步骤# === submodule 在 CI 中 ===
# GitHub Actions
- uses: actions/checkout@v4
with:
submodules: recursive # 必须显式指定!
# 或 submodules: true
# 如果子模块是私有仓库,还需要配置额外的认证
# fetch-depth: 0 可能也需要
# === subtree 在 CI 中 ===
# GitHub Actions
- uses: actions/checkout@v4
# 就这样,不需要额外配置# === submodule 版本锁定 ===
# 精确锁定到特定 SHA,非常清晰
git ls-tree HEAD libs/shared
# 160000 commit abc1234... libs/shared
# 明确知道使用的是哪个版本
# === subtree 版本锁定 ===
# 不太直观,需要查看合并提交
git log --oneline --grep="subtree" libs/shared
# 需要手动管理版本信息# 如果子项目是通过包管理器分发的,优先用包管理器
npm install shared-lib # Node.js
pip install shared-lib # Python
cargo add shared-lib # Rust
go get github.com/team/lib # Go
# 包管理器的优势:
# - 版本管理更成熟(语义化版本、lockfile)
# - 依赖解析(传递依赖、版本冲突)
# - 社区生态(npm registry、PyPI)
# - 构建集成(无需额外配置)# 微前端项目结构
# main-app/
# micro-apps/
# header/ ← submodule(独立团队维护)
# sidebar/ ← submodule(独立团队维护)
# dashboard/ ← submodule(独立团队维护)
# shell/ ← 主应用壳
# 添加各微前端子模块
git submodule add -b main https://github.com/team/header.git micro-apps/header
git submodule add -b main https://github.com/team/sidebar.git micro-apps/sidebar
git submodule add -b main https://github.com/team/dashboard.git micro-apps/dashboard
# 各团队独立开发,主应用定期同步
git submodule update --remote --merge
# CI 中构建所有子模块
git submodule foreach 'npm install && npm run build'# 多个微服务共享 Protobuf 定义
# service-a/
# proto/shared/ ← subtree(共享的 proto 定义)
# src/
# service-b/
# proto/shared/ ← subtree(同一来源)
# service-a 添加
cd service-a
git remote add proto-repo https://github.com/team/proto-definitions.git
git subtree add --prefix=proto/shared proto-repo main --squash
# service-b 添加
cd ../service-b
git remote add proto-repo https://github.com/team/proto-definitions.git
git subtree add --prefix=proto/shared proto-repo main --squash
# 有人修改了 proto 定义,推送到 proto-repo
# 各服务同步更新
cd service-a
git subtree pull --prefix=proto/shared proto-repo main --squash
# 如果在 service-a 中改了 proto,推送回共享仓库
git subtree push --prefix=proto/shared proto-repo feature/new-message# CI 中测试不同版本的子模块组合
# .github/workflows/test-matrix.yml
# name: Test with different lib versions
# strategy:
# matrix:
# lib-version: [v1.0, v2.0, v3.0]
# steps:
# - uses: actions/checkout@v4
# with:
# submodules: true
# - name: Switch submodule version
# run: |
# cd libs/shared
# git fetch --tags
# git checkout ${{ matrix.lib-version }}
# - name: Run tests
# run: npm test
# 本地也可以快速切换子模块版本测试
cd libs/shared
git checkout v1.0 && cd ../.. && npm test
cd libs/shared
git checkout v2.0 && cd ../.. && npm test# 当项目规模增长,subtree 不再适合时迁移到 submodule
# 1. 记录当前 subtree 的远程信息
REMOTE_URL="https://github.com/team/shared-lib.git"
PREFIX="libs/shared"
# 2. 拆分 subtree 的历史到独立分支(确保远程仓库有最新代码)
git subtree push --prefix=$PREFIX $REMOTE_URL main
# 3. 删除 subtree 目录
git rm -r $PREFIX
git commit -m "chore: 移除 shared-lib subtree,准备迁移到 submodule"
# 4. 添加为 submodule
git submodule add $REMOTE_URL $PREFIX
git commit -m "chore: 将 shared-lib 从 subtree 迁移到 submodule"
# 5. 通知团队
# - 所有人需要重新 clone 或执行 git submodule update --init# 使用 submodule 管理 monorepo 中的外部依赖
# 同时用 Turborepo/Nx/Lerna 管理内部包
# 项目结构
# monorepo/
# packages/
# app/
# api/
# shared/
# external/
# design-system/ ← submodule(外部团队维护的设计系统)
# proto/ ← submodule(共享的 proto 定义)
# turbo.json
# turbo.json 中配置包含 submodule 的依赖
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "../external/**"]
}
}
}
# 更新外部依赖时触发相关包的重新构建
git submodule update --remote external/design-system
# Turborepo 会检测到 external/ 的变化,重新构建依赖它的包# 错误 1:fatal: no submodule mapping found in .gitmodules
# 原因:.gitmodules 中缺少对应的子模块配置
# 修复:
git submodule sync
# 如果仍然报错,手动编辑 .gitmodules 添加缺失的条目
# 错误 2:fatal: reference is not a tree
# 原因:主仓库记录的子模块 SHA 在子模块仓库中不存在
# 可能是子模块做了 force push 重写历史
# 修复:
cd libs/shared
git fetch origin
git log --all --oneline | head -20
# 找到正确的 SHA,回到主仓库更新引用
cd ../..
git add libs/shared
git commit -m "fix: 更新子模块引用到正确的 SHA"
# 错误 3:clone 后子模块目录存在但是空的
git submodule update --init --force
# 错误 4:子模块的远程 URL 变了
git submodule sync --recursive
git submodule update --init --recursive
# 诊断命令
git submodule status --recursive
git config --file .gitmodules --list# ❌ 直接克隆后子模块目录是空的
git clone https://github.com/team/project.git
cd project/libs/shared # 空的!
# ✅ 克隆时初始化
git clone --recurse-submodules https://github.com/team/project.git
# ✅ 或事后初始化
git submodule update --init --recursive# ❌ 子模块默认是 detached HEAD
cd libs/shared
vim src/utils.ts
git commit -am "fix" # 提交在 detached HEAD 上!
cd ../..
git submodule update # 悬空提交可能丢失!
# ✅ 先切换到分支
cd libs/shared
git switch main
# 修改并提交# ❌ 更新了子模块但忘记提交引用变更
git submodule update --remote
# libs/shared 更新到了新版本
# 但主仓库还没记录这个变更!
git push # 推送了旧的引用
# ✅ 更新后提交
git submodule update --remote
git add libs/shared
git commit -m "chore: 更新 shared-lib"
git push--squash 一致性# ❌ add 时用了 --squash,pull 时没用
git subtree add --prefix=libs/shared shared-lib main --squash
git subtree pull --prefix=libs/shared shared-lib main
# 可能产生合并问题!
# ✅ 始终保持一致
git subtree add --prefix=libs/shared shared-lib main --squash
git subtree pull --prefix=libs/shared shared-lib main --squash# subtree push 需要遍历整个主仓库历史,在大仓库中可能很慢
git subtree push --prefix=libs/shared shared-lib main
# 如果主仓库有几万个提交,这可能需要几分钟
# 优化方案:先 split 再 push
git subtree split --prefix=libs/shared -b temp-split
git push shared-lib temp-split:main
git branch -D temp-split# 子模块仓库迁移了 URL
# 需要更新 .gitmodules 和本地配置
git config -f .gitmodules submodule.libs/shared.url https://new-url.git
git submodule sync
git submodule update --init --recursive
git commit -am "chore: 更新子模块 URL"技术上可以,但不推荐。在同一个项目中使用两种方案会增加团队的认知负担。建议统一选择一种方案。
# 1. 记录子模块信息
URL=$(git config -f .gitmodules submodule.libs/shared.url)
BRANCH=$(git config -f .gitmodules submodule.libs/shared.branch || echo main)
# 2. 删除子模块
git submodule deinit -f libs/shared
git rm -f libs/shared
rm -rf .git/modules/libs/shared
git commit -m "chore: 移除 shared-lib 子模块"
# 3. 添加为 subtree
git remote add shared-lib $URL
git subtree add --prefix=libs/shared shared-lib $BRANCH --squash# 1. 在子模块中提交并推送
cd libs/shared
git switch main
git commit -am "feat: 新功能"
git push origin main
# 2. 在主仓库中更新子模块引用并推送
cd ../..
git add libs/shared
git commit -m "chore: 更新 shared-lib"
git push
# 3. 其他项目执行更新
git submodule update --remote libs/shared
git add libs/shared
git commit -m "chore: 同步 shared-lib 更新"subtree 不像 submodule 那样有清晰的版本锁定机制。推荐做法:
# 在 subtree add/pull 的提交信息中记录版本
git subtree pull --prefix=libs/shared shared-lib v2.0.0 --squash \
-m "chore: 更新 shared-lib 到 v2.0.0"
# 查看历史
git log --oneline --grep="shared-lib"# submodule 路径使用正斜杠
git submodule add https://github.com/team/lib.git libs/shared
# subtree 的 --prefix 也使用正斜杠
git subtree add --prefix=libs/shared shared-lib main --squash
# 删除子模块时使用 Remove-Item
Remove-Item -Recurse -Force .git/modules/libs/shared# submodule 可以锁定到任何引用(标签、分支、SHA)
cd libs/shared
git fetch --tags
git checkout v2.0.0
cd ../..
git add libs/shared
git commit -m "chore: 锁定 shared-lib 到 v2.0.0"
# 但自动更新(--remote)只跟踪分支,不跟踪标签
# 升级时需要手动 checkout 新标签[alias]
# submodule
smu = submodule update --init --recursive
smr = submodule update --remote --merge
sms = submodule status
smf = submodule foreach
# subtree
sta = "!f() { git subtree add --prefix=$1 $2 ${3:-main} --squash; }; f"
stp = "!f() { git subtree pull --prefix=$1 $2 ${3:-main} --squash; }; f"
stpush = "!f() { git subtree push --prefix=$1 $2 ${3:-main}; }; f"
# 使用
git smu # 初始化所有子模块
git smr # 更新所有子模块到最新
git smf 'git status' # 查看所有子模块状态
git sta libs/shared shared-lib main # 添加 subtree
git stp libs/shared shared-lib main # 更新 subtree# 配置克隆时自动初始化子模块
git config --global submodule.recurse true
# 之后 git clone、git pull、git checkout 都会自动处理子模块
# 或在项目中添加 post-checkout hook
# .git/hooks/post-checkout
#!/bin/bash
git submodule update --init --recursive# 克隆时只获取子模块的最新提交(节省时间和空间)
git clone --recurse-submodules --shallow-submodules https://github.com/team/project.git
# 或配置
git config -f .gitmodules submodule.libs/shared.shallow true#!/bin/bash
# update-subtrees.sh — 批量更新所有 subtree
SUBTREES=(
"libs/shared:shared-lib:main"
"libs/utils:utils-lib:main"
"config/shared:configs:main"
)
for entry in "${SUBTREES[@]}"; do
IFS=":" read -r prefix remote branch <<< "$entry"
echo "=== 更新 $prefix (from $remote/$branch) ==="
git subtree pull --prefix=$prefix $remote $branch --squash -m "chore: 更新 $prefix"
echo ""
done
echo "✅ 所有 subtree 已更新"#!/bin/bash
# check-submodules.sh — 检查所有子模块的状态
echo "=== 子模块状态检查 ==="
echo ""
git submodule foreach --quiet '
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "detached")
STATUS=""
if [ "$BRANCH" = "HEAD" ]; then
STATUS="⚠️ DETACHED HEAD"
else
AHEAD=$(git rev-list --count HEAD..origin/$BRANCH 2>/dev/null || echo "?")
BEHIND=$(git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo "?")
if [ "$AHEAD" != "0" ] || [ "$BEHIND" != "0" ]; then
STATUS="ahead $BEHIND, behind $AHEAD"
else
STATUS="✅ 同步"
fi
fi
printf "%-30s %-15s %s\n" "$name" "[$BRANCH]" "$STATUS"
'| 目标 | 命令 |
|---|---|
| 添加子模块 | git submodule add <url> <path> |
| 初始化 | git submodule init |
| 更新到记录的版本 | git submodule update |
| 初始化并更新 | git submodule update --init --recursive |
| 更新到远程最新 | git submodule update --remote |
| 查看状态 | git submodule status |
| 对所有子模块执行命令 | git submodule foreach '<cmd>' |
| 删除子模块 | git submodule deinit -f <path> + git rm <path> |
| 同步 URL 变更 | git submodule sync |
| 克隆时初始化 | git clone --recurse-submodules <url> |
| 目标 | 命令 |
|---|---|
| 添加 subtree | git subtree add --prefix=<path> <remote> <branch> --squash |
| 更新 subtree | git subtree pull --prefix=<path> <remote> <branch> --squash |
| 推送到子仓库 | git subtree push --prefix=<path> <remote> <branch> |
| 拆分子目录历史 | git subtree split --prefix=<path> -b <branch> |