🗂 目录

git 对多模块的支持

January 8, 2023 • 预计阅读时间 5 分钟

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 下面的 web/、serviceA/、serviceB/ 都是空的,需要跑:

❯ git submodule update --init --recursive

才能把 web、serviceA、serviceB 下载到本地。

在 myapp 里增加一个子模块 serviceC:

❯ git submodule add <serviceC_remote_repo> serviceC 

submodule 配置信息包含在 .gitmodules 里面:

❯ cat .gitmodules 

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(Using Git: What is a “Detached HEAD”? )。

serviceC/ 就是一普通的 git repo,可遵循之前介绍的 git flow 进行开发,但 serviceC 的改变每次需要做 两种 commit/push,一次是针对 serviceC (myapp/serviceC/.git 以及 serviceC_remote_repo),另一次是针对 myapp (myapp/.git/modules/serviceC 以及 myapp_remote_repo)。

同样如果另一开发人员对 serviceC 做了更新,可以通过 myapp_remote_repo 或者 serviceC_remote_repo 获得:

❯ git submodule update --remote --merge

--remote 将从 serviceC_remote_repo (develop/默认分支上) 同步更新,否则从 myapp_remote_repo 同步更新。如果不是默认分支,可以修改 submodule 配置:

❯ git config -f .gitmodules submodule.<submodule>.branch <branch>

myapp 和 serviceC 分属两个 git repo,这是 submodule 协同开发特别需要注意的地方,简化建议是,开发 serviceC 时遵循之前介绍的 git flow ,同时 commit/push myapp;当使用 myapp 时,serviceC 保持在 develop 分支上,只接受更新,不做开发。如果无法获得最近的更新,可以通过删除 submodule 然后再加回来解决。

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 。

DevOpsgit

  上一篇:Dockerfile 镜像构建最佳实践

  下一篇:2022年技术书单

comments powered by Disqus