跳转到内容
关系图谱

Git submodule 与 subtree 详解

5,015 字 20 分钟

属性 1
status
学习中

Git submodule 与 subtree 详解

git submodulegit subtree 是 Git 中管理子项目依赖的两种方案。它们解决同一个问题:如何在一个仓库中引用另一个仓库的代码,但设计理念和使用方式截然不同。

一句话理解

submodule链接(记录指向另一个仓库的指针),subtree复制(将另一个仓库的代码合并进来)。


核心对比

对比项submodulesubtree
数据存储只存引用(SHA),代码在独立仓库代码直接合并到主仓库
克隆体验需要 --recurse-submodules普通克隆即可获得全部代码
更新方式git submodule updategit subtree pull
贡献回推在子模块目录中独立 pushgit subtree push
学习曲线较陡(概念多、易出错)较平缓
历史记录子项目历史独立子项目历史混入主仓库
.gitmodules 文件需要不需要
适用场景大型子项目、需要独立版本控制小型库、vendor 代码

Part 1: Git submodule

基本概念

submodule 让你在一个 Git 仓库中嵌入另一个 Git 仓库。主仓库只记录子模块的仓库 URL特定提交 SHA,不直接包含子模块的代码文件。

submodule 的组成

bash
# 添加 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 = 子模块类型

添加 submodule

bash
# 添加子模块
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 子模块"

克隆含 submodule 的仓库

bash
# 方式 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,子模块目录会是空的!这是很多新手困惑的来源。

更新 submodule

更新到主仓库记录的版本

bash
# 当你切换分支、pull 更新后,子模块可能需要同步
git submodule update

# 递归更新
git submodule update --recursive

# 常见场景:pull 后子模块指针变了
git pull
# 提示 libs/shared 有变更
git submodule update

更新到子模块的最新版本

bash
# 将子模块更新到远程的最新提交
git submodule update --remote
# 默认跟踪 master/main 分支

# 更新特定子模块
git submodule update --remote libs/shared

# 更新后需要提交主仓库的引用变更
git add libs/shared
git commit -m "chore: 更新 shared-lib 到最新版本"

配置跟踪的分支

bash
# 在 .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

在 submodule 中工作

bash
# 进入子模块目录
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

直接在子模块中提交而不先切换到分支,提交会成为悬空提交,可能被垃圾回收丢失!

bash
# ❌ 在 detached HEAD 上提交
cd libs/shared
git commit -am "fix"   # 悬空提交!

# ✅ 先切换到分支
cd libs/shared
git switch main
git commit -am "fix"
git push

删除 submodule

删除子模块比添加复杂,需要清理多处:

bash
# 步骤 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 2.12+ 简化了删除
bash
git rm libs/shared
rm -rf .git/modules/libs/shared
git commit -m "chore: 移除 shared-lib 子模块"

submodule 常用命令

bash
# 查看所有子模块状态
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

submodule 实际场景

场景 1:共享内部库

bash
# 多个项目共享一个内部工具库
# 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 更新"

场景 2:锁定第三方库版本

bash
# 将第三方库锁定到特定版本
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"

场景 3:Monorepo 中的独立组件

bash
# 微服务架构中,某些服务需要共享 proto 定义
# main-project/
#   proto/           ← 子模块(共享的 protobuf 定义)
#   service-a/
#   service-b/

# 配置 CI 自动更新子模块
# .github/workflows/ci.yml
# steps:
#   - uses: actions/checkout@v4
#     with:
#       submodules: recursive

Part 2: Git subtree

基本概念

subtree 将另一个仓库的代码直接合并到主仓库的某个子目录中。代码成为主仓库的一部分,不需要额外的初始化步骤。

添加 subtree

bash
# 添加远程仓库(可选但推荐,方便后续操作)
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,后续的 pullpush 也必须始终使用 --squash

更新 subtree

bash
# 从远程拉取子仓库的最新代码
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

推送修改回子仓库

bash
# 如果你在主仓库中修改了 subtree 目录下的代码,可以推送回原始仓库
git subtree push --prefix=libs/shared shared-lib main

# 使用 --squash 推送
git subtree push --prefix=libs/shared shared-lib feature/update --squash
subtree push 的工作原理

subtree push 会遍历主仓库的提交历史,找出所有影响了 --prefix 目录的提交,将它们重新组织为只包含子目录修改的提交,然后推送到子仓库。

拆分 subtree

bash
# 将子目录的提交历史拆分为一个独立的分支
git subtree split --prefix=libs/shared -b shared-branch

# 然后可以推送到独立仓库
git push shared-lib shared-branch:main

subtree 实际场景

场景 1:引入第三方库(vendor)

bash
# 将第三方库的代码直接纳入项目
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

场景 2:从 Monorepo 拆分项目

bash
# 从 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

场景 3:共享配置文件

bash
# 多个项目共享 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

Part 3: 深入对比与选择指南

详细对比

团队体验对比

bash
# === 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/    # 所有文件都在
# 无需额外步骤

CI/CD 对比

yaml
# === submodule 在 CI 中 ===
# GitHub Actions
- uses: actions/checkout@v4
  with:
    submodules: recursive # 必须显式指定!
    # 或 submodules: true

# 如果子模块是私有仓库,还需要配置额外的认证
# fetch-depth: 0 可能也需要

# === subtree 在 CI 中 ===
# GitHub Actions
- uses: actions/checkout@v4
# 就这样,不需要额外配置

版本锁定对比

bash
# === submodule 版本锁定 ===
# 精确锁定到特定 SHA,非常清晰
git ls-tree HEAD libs/shared
# 160000 commit abc1234... libs/shared
# 明确知道使用的是哪个版本

# === subtree 版本锁定 ===
# 不太直观,需要查看合并提交
git log --oneline --grep="subtree" libs/shared
# 需要手动管理版本信息

选择决策流程

何时选 submodule

  • 子项目是大型独立项目,有自己的版本发布周期
  • 需要在子项目中频繁开发并推送
  • 需要精确锁定子项目版本
  • 子项目被多个主项目引用,需要独立管理
  • 团队对 Git 比较熟悉

何时选 subtree

  • 子项目是小型库或配置
  • 想要简化团队的克隆和 CI 流程
  • 不需要频繁更新子项目
  • 团队 Git 经验较少
  • 需要离线工作时也能访问子项目代码

何时都不选

bash
# 如果子项目是通过包管理器分发的,优先用包管理器
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)
# - 构建集成(无需额外配置)

更多实战场景

场景 4:submodule 用于微前端架构

bash
# 微前端项目结构
# 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'

场景 5:subtree 管理共享 proto/schema

bash
# 多个微服务共享 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

场景 6:submodule 版本矩阵测试

bash
# 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

场景 7:从 subtree 迁移到 submodule

bash
# 当项目规模增长,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

场景 8:submodule 与 monorepo 工具结合

bash
# 使用 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/ 的变化,重新构建依赖它的包

submodule 故障排查

常见错误与修复

bash
# 错误 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

常见陷阱

陷阱 1:submodule 忘记初始化
bash
# ❌ 直接克隆后子模块目录是空的
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
陷阱 2:submodule 中在 detached HEAD 上提交
bash
# ❌ 子模块默认是 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
# 修改并提交
陷阱 3:submodule 更新后忘记提交主仓库
bash
# ❌ 更新了子模块但忘记提交引用变更
git submodule update --remote
# libs/shared 更新到了新版本
# 但主仓库还没记录这个变更!
git push   # 推送了旧的引用

# ✅ 更新后提交
git submodule update --remote
git add libs/shared
git commit -m "chore: 更新 shared-lib"
git push
陷阱 4:subtree 的 --squash 一致性
bash
# ❌ 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
陷阱 5:subtree push 在大仓库中很慢
bash
# 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
陷阱 6:submodule URL 变更后同步
bash
# 子模块仓库迁移了 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"

常见问题

submodule 和 subtree 可以混用吗?

技术上可以,但不推荐。在同一个项目中使用两种方案会增加团队的认知负担。建议统一选择一种方案。

如何将 submodule 转换为 subtree?
bash
# 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
submodule 中的修改如何被其他项目获取?
bash
# 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 如何查看当前使用的版本?

subtree 不像 submodule 那样有清晰的版本锁定机制。推荐做法:

bash
# 在 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"
在 Windows PowerShell 中的注意事项?
powershell
# 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
子模块可以跟踪标签而非分支吗?
bash
# 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 新标签

实用技巧

技巧 1:Git alias 简化操作

bash
[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

技巧 2:自动初始化 submodule

bash
# 配置克隆时自动初始化子模块
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

技巧 3:shallow submodule(浅子模块)

bash
# 克隆时只获取子模块的最新提交(节省时间和空间)
git clone --recurse-submodules --shallow-submodules https://github.com/team/project.git

# 或配置
git config -f .gitmodules submodule.libs/shared.shallow true

技巧 4:subtree 更新脚本

bash
#!/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 已更新"

技巧 5:submodule 状态检查脚本

bash
#!/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

目标命令
添加子模块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>

git subtree

目标命令
添加 subtreegit subtree add --prefix=<path> <remote> <branch> --squash
更新 subtreegit subtree pull --prefix=<path> <remote> <branch> --squash
推送到子仓库git subtree push --prefix=<path> <remote> <branch>
拆分子目录历史git subtree split --prefix=<path> -b <branch>

相关命令

参考资料