Git 多模块的支持
Table of Contents
多模块 #
一个大型项目,特别是微服务,往往都包含多个子项目/多模块 - web、component1、serviceA、serviceB 等等,component 是 service 依赖的通用组件,它们通常各自有各自的 git repo(可以全部放在单一的 mono repo,但这种情况很少),但作为整体有时候需要进行统一管理,例如要在本地进行全系统开发和测试,如果 web、component1、component2、serviceA、serviceB 在各自独立的文件目录下,我们就要它们之间不停的手动切换。另一方面,java, javascript,go 等可以通过 artifact/lib 来进行共享或调用,但 devops 的实现是 scripts、yml 等,只能通过文件目录来共享或调用。git 是否能解决这一问题呢?答案就是 submodule
这个类似 linux soft link directory, 例如 myapp 的 git repo 存放的是:
├── myapp/
| ├── .git/
| ├── .gitmodules
| ├── deploy/
| │ ├── base/
| │ │ ├── cert-manager/
| │ │ ├── ingress/
| │ │ ├── kustomization.yaml
| │ ├── envs/
| ├── docker-compose.yml
| ├── serviceA → serviceA-repo
| ├── serviceB → serviceB-repo
| ├── web → web-repo
| ...
web、serviceA、serviceB 是 myapp 的 submodule,component1 是 serviceA/B 的 submodule,实际使用时,文件目录可变成:
├── myapp/
| ├── .git/
| ├── .gitmodules
| ├── deploy/
| │ ├── base/
| │ │ ├── cert-manager/
| │ │ ├── ingress/
| │ │ ├── kustomization.yaml
| │ ├── envs/
| ├── docker-compose.yml
| ├── serviceA
| | ├── .git
| │ ├── component1/
| | | ├── .git
| │ │ ├── src/
| │ │ │ ├── main/
| │ │ │ ├── test/
| │ │ ├── build.gradle
| │ ├── src/
| │ │ ├── main/
| │ │ ├── test/
| │ ├── build.gradle
| │ ├── Dockerfile
| ├── serviceB
| | ├── .git
| │ ├── component1/
| | | ├── .git
| │ │ ├── src/
| │ │ │ ├── main/
| │ │ │ ├── test/
| │ │ ├── build.gradle
| │ ├── src/
| │ │ ├── main/
| │ │ ├── test/
| │ ├── build.gradle
| │ ├── Dockerfile
| ├── web
| | ├── .git
| │ ├── src/
| │ │ ├── assets/
| │ │ ├── components/
| │ │ ├── context/
| │ │ ├── features/
| │ │ ├── hooks/
| │ │ ├── libs/
| │ │ ├── pages/
| │ │ ├── services/
| │ │ ├── utils/
| │ ├── App.js
| │ ├── Dockerfile
| ...
有不同的方式把 service/module/component 捆绑在一起:
- Intellij 和 其他编辑器往往允许不同的项目集成在一个的 workspace 里,这样工作时就不需要同时为每个 service/module/component 开个窗口;
- Maven/Gradle 对多 module 支持,这是在编程、构建、测试、部署时可以作为一个“强”结合,可以对 service/module/component 的编程、构建、测试、部署统一配置(DRY);
- 使用 git
submodule
各个 service/module/component 在编程、构建、测试、部署时是可以独立的,但是也作为一个完整系统部署,测试 - 使用 git
subtree
- 使用
monorepo
- Maven 对多模块支持比较清楚(通过 inheritance),Gradle 通过 plugin 来实现,灵活但是需要花点时间学习
- 方式 3 的配置比较复杂,多团队大型系统开发使用 3
- 方式 2 & 3 可以结合使用
- 方式 5 只用一个 git repo,CI/CD 需要针对性的工具来支持单个模块打包、部署等
submodule #
第一次 git clone myapp 到本地后,myapp 下面 submodule 的文件夹 web/、serviceA/、serviceB/ 都是空的,需要跑:
❯ git submodule update --init --recursive
才能把 web、serviceA、serviceB 下载到本地。
在 myapp 里增加一个子模块 serviceC:
❯ git submodule add <serviceC_remote_repo> serviceC
如果不是默认分支,可以修改 submodule 配置:
❯ git config -f .gitmodules submodule.<submodule>.branch <branch>
submodule 配置信息包含在 .gitmodules 里面:
❯ cat .gitmodules
添加或删除一个 submodule:
f_git_mod_add() {
git submodule add "$1" "$2"
git submodule init
git submodule update --remove --recursive
}
alias gmadd="f_git_mod_add"
f_git_mod_del() {
git submodule deinit -f "$1"
rm -rf .git/modules/"$1"
git rm -f "$1"
}
alias gmdel="f_git_mod_del"
git 的管理文件都放在 .git 目录下,同样的 serviceC 的 git 管理文件都放在 myapp/.git/modules/servicesC 下,但 myapp/.git/modules/servicesC 和通常的 .git (myapp/serviceC/.git/) 区别是只包含 SHA-1 commits, branch、tag 等信息是没有追踪和保存的,这时的 HEAD 直接指向 commit 而非 branch,称之为 detached HEAD:
serviceC/ 就是一普通的 git repo,可遵循之前介绍的 经历了好几个公司和开发团队,git 的使用流程依旧不清晰,这里重点是采用最多的 feature 开发实战流程
同样如果另一开发人员对 serviceC 做了更新,可以通过 myapp remote repo 或者 serviceC remote repo 获得:
git submodule update --remove --recursive --merge
--remote 将从 serviceC remote repo (develop/默认分支上) 同步更新,否则从 myapp remote repo 同步更新。
myapp 和 serviceC 分属两个 git repo,这是 submodule 协同开发特别需要注意的地方,serviceC 有本地更新时,如果也直接更新 myapp,serviceC 的分支 commit# 直接进入 myapp/.git/modules/servicesC 在另外一个人的机器上就会有 commit# 冲突,但是 commit# 是存在 .git 里无法用通常的手段 merge,可以在 serviceC 更新提交 PR 后,pull develop 分支过来后才 commit myapp,这个比较麻烦,而且 servicesC 提交的更新不一定来自本地,所以简洁做法是开发 serviceC 时遵循之前介绍的:
经历了好几个公司和开发团队,git 的使用流程依旧不清晰,这里重点是采用最多的 feature 开发实战流程
只是对 submodule 的修改,不需要对 myapp 的 做 git 更新;当使用 myapp 时,所有 submodule 保持在 develop 分支上,一次性通过上面的命令更新所有本地 submodule,然后才更新 myapp。如果有问题,最终都可以通过删除 submodule 然后再加回来解决。
# git submodule
alias gm="git submodule"
f_git_mod_update() {
git submodule foreach --recursive git checkout develop
git submodule foreach --recursive git pull origin develop
git add -A
git commit -m "update submodules $(date +'%d/%m/%Y')"
}
alias gmupd="f_git_mod_update"
简单讲更新顺序就是:
本地 service F 分支 → remote service F 分支 → remote service D 分支 → 本地 app F 分支 → remote app F 分支 → remote app D 分支 |
subtree #
maven,gradle 能在单一项目里支持多模块,git 是否也能制订这种“强”依赖关系,而不是靠 submodule 这种“软”链接呢?答案是 subtree
:
❯ git subtree
usage: git subtree add --prefix=<prefix> <commit>
or: git subtree add --prefix=<prefix> <repository> <ref>
or: git subtree merge --prefix=<prefix> <commit>
or: git subtree split --prefix=<prefix> [<commit>]
or: git subtree pull --prefix=<prefix> <repository> <ref>
or: git subtree push --prefix=<prefix> <repository> <refspec>
❯ git remote add serviceC git@github.com:fastzhong/serviceC.git
❯ git subtree add --prefix=subtree serviceC master
执行 git subtree add 之后,serviceC 所有的 commit 都会 merge 进 myapp,serviceC 也随之纳入 myapp 的 git 管理,所以这时只有 3 个 git repo,myapp 本地 + remote,以及 serviceC remote。serviceC 的协同可以通过 git subtree 的 pull/push 来完成 - push 时,git 会把不属于 serviceC 的 commit 过滤掉;pull 时,serviceC remote 上新的 commit merge 到 myapp,一切显得那么自然,但是。
subtree 问题 #
使用 –squash 参数
就是把 subtree 子项目的更新记录进行合并,再合并到主项目中:subtree add 或者 pull 操作的结果对应两个 commit, 一个是 squash 了子项目的历史记录, 一个是 merge 到主项目中。
优点:主项目的历史记录看起来还是比较整齐的。
缺点:在子项目需要 subtree pull 的时候,经常需要处理冲突,甚至每次 subtree pull 的时候都需要重复处理同样的冲突。《原因》subtree add/pull 操作中,需要用到 merge,而 merge 顺利进行的前提, 是要有相同的 parent commit。原子项目历史记录被合并后就消失了,相当于一个“新”的提交。 下次再进行 add/pull 时,新添加的内容找不到“上一次的修改”, 于是在更新 subtree 内文件的时候,就会提示冲突,需要手工解决。
不使用 –squash 参数
优点:子项目更新的时候,subtree pull 很顺利, 能够自动处理已解决过的冲突。《原因》原子项目的历史复制到了父项目中, 下次再进行 add/pull 时,新增的 commit 能够找到“上一次的修改”, 那么他会像在子项目中逐个 patch 那样更新 subtree 下的内容, 不会提示冲突。
缺点:子项目的更新记录“污染”了主项目的。
是否使用 squash 都是可以的, 但需要在开始阶段作出选择,并 一直坚持下去 。 如果一会儿用一会儿不用,得到的不是两者的优点,而是两者的缺点之和。解决方法参考: Git subtree 要不要使用 –squash 参数
个人认为 myapp 和 serviceC 的历史记录应该分开管理,所以倾向 submodule 而不是 subtree 。如果要统一管理,采用 Monorepo。