Git Submodule实战避坑指南与高效协作技巧

博主钰岩 工具 阅读 2,680
赞 11 收藏
二维码
手机扫码查看
反馈

先看效果,再看代码

上周给一个老项目加新功能,得复用另一个团队维护的 UI 组件库。他们不发 npm 包,也不开放私有 registry 权限,只给了个 Git 仓库地址:git@github.com:team-x/ui-kit.git。我第一反应是:clone 下来,手动拷进 src/lib?不行,后续更新太痛苦,每次都要 diff、merge、手改路径……折腾半天发现,Git Submodule 就是为这种场景生的。

Git Submodule实战避坑指南与高效协作技巧

直接上命令,我本地实测有效的流程(Mac + Git 2.39):

# 进入你的主项目根目录
cd my-main-project

# 添加 submodule(会自动 clone,并在 .gitmodules 里记一笔)
git submodule add git@github.com:team-x/ui-kit.git src/lib/ui-kit

# 提交——注意:submodule 的 commit hash 会被记在主 repo 的 tree 里
git add .gitmodules src/lib/ui-kit
git commit -m "feat: 引入 team-x/ui-kit 作为 submodule"

# 推送到远端后,别人 clone 时默认不会拉 submodule
# 所以得教队友这么用:
git clone --recurse-submodules git@github.com:me/my-main-project.git
# 或者已 clone 完了,补拉:
git submodule update --init --recursive

这个场景最好用

Submodule 最香的地方,不是“能复用代码”,而是版本锁定+解耦维护。比如我们有个内部监控 SDK,三个前端项目都用它,但每个项目依赖的版本不同:

  • 项目 A 要稳定,锁死在 v1.2.0(对应 commit a1b2c3d
  • 项目 B 在灰度新功能,用 v1.3.0-beta.2e4f5g6h
  • 项目 C 还在开发中,直接跟踪 main 分支最新提交

用 submodule,三项目互不干扰。A 项目里 src/sdk 指向 a1b2c3d,B 指向 e4f5g6h,C 可以随时 git submodule update --remote 拉最新。npm install?不用。发包?不用。只要 git push/pull 就完事。

附一个我常用的更新脚本(放在主项目根目录的 update-sdk.sh):

#!/bin/bash
# 更新 submodule 并自动 commit(适合 CI 或日常快速同步)
cd src/sdk
git fetch origin
git checkout origin/main  # 或指定 tag:git checkout v1.3.0
cd ..
git add src/sdk
git commit -m "chore(sdk): update to latest main"

踩坑提醒:这三点一定注意

第一,别乱删 .gitmodules 里的路径——我上次删错了一行,结果 git status 一直显示 modified: src/lib/ui-kit (new commits),但 git diff 啥也看不到。折腾半小时才发现是 .gitmodules 里路径写成 src/lib/ui_kit(下划线),而实际文件夹是 ui-kit(短横线)。Git 把它当两个 submodule 处理了。亲测有效修复方式:

# 先删掉错误记录
git rm --cached src/lib/ui_kit
# 再重新 add 正确路径
git submodule add git@github.com:team-x/ui-kit.git src/lib/ui-kit
# 最后 commit
git commit -m "fix: correct submodule path in .gitmodules"

第二,CI 构建失败?大概率是没配 –recurse-submodules。GitHub Actions 默认不拉 submodule。必须显式声明:

- uses: actions/checkout@v4
  with:
    submodules: 'recursive'  # 关键!别写 true,要写 'recursive'

第三,别在 submodule 里改东西还忘了 push。这是最隐蔽的坑:你在 src/lib/ui-kit 里修了个 bug,commit 了,但没 git push 到上游仓库。然后你 git commit -m "fix ui-kit" 提交到主项目,CI 跑起来却还是旧版——因为 submodule 记录的是本地 commit hash,但别人 git submodule update 时,那个 hash 在远端仓库根本不存在,直接报错 fatal: reference is not a tree。我的习惯是:改完 submodule,立刻 push,再回主项目 commit。

高级技巧:怎么让它“像 npm 包一样用”?

很多人卡在“怎么 import?”——别慌,submodule 就是普通文件夹,照常 import 就行。比如 ui-kit 里有 src/index.ts,你就可以:

// src/pages/Home.tsx
import { Button } from 'src/lib/ui-kit/src/index'
// 或更干脆,配 alias(vite.config.ts):
export default defineConfig({
  resolve: {
    alias: {
      '@ui-kit': path.resolve(__dirname, 'src/lib/ui-kit/src')
    }
  }
})
// 然后:
import { Button } from '@ui-kit'

还有个骚操作:用 post-checkout hook 自动初始化 submodule(适合团队新人):

# .git/hooks/post-checkout(需 chmod +x)
#!/bin/bash
if [ "$3" = "1" ]; then
  git submodule update --init --recursive 2>/dev/null || true
fi

不过注意:hook 不会自动同步到队友本地,得靠文档或 pre-commit 提示。

它不是万能的,但够用

Submodule 不能替代 monorepo,也不解决依赖冲突。比如你主项目用了 React 18,submodule 里用了 React 17,打包时可能双份 runtime。这时候就得靠 peerDependencies 约束 + resolutions(yarn)或 overrides(pnpm)硬指定。但我试过,只要 submodule 本身没把 React 打进 dist,问题不大。

另外,submodule 无法像 npm 那样做 semantic versioning 自动升级(比如 ^1.2.0),版本管理全靠人肉 commit hash。所以我们的做法是:所有 submodule 的更新,必须带清晰的 commit message,比如 "chore(ui-kit): update to v1.3.0 (a1b2c3d)",方便追溯。

结尾

以上是我过去一年在 4 个项目里用 submodule 的真实经验。它不是什么黑科技,就是个“Git 管理 Git”的朴素方案,但胜在稳定、透明、零构建侵入。如果你正被第三方私有组件库、跨团队共享逻辑、或者历史遗留模块复用搞得头大,不妨试试——比手拷、比 patch、比 fork 后定期 merge 都省心。

这个技巧的拓展用法还有很多,比如结合 git subtree 做单向同步,或者用 git worktree 辅助 submodule 开发调试,后续会继续分享这类博客。

以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论