Git 多模块的支持
Table of Contents
submodule 介绍 #
一个大型项目,特别是微服务,往往都包含多个子项目/多模块 - 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
| ...
使用 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 开发实战流程
# 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"
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 。