Skip to main content

Git 多模块的支持

·3015 字·7 分钟
开发 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 捆绑在一起:

  1. Intellij 和 其他编辑器往往允许不同的项目集成在一个的 workspace 里,这样工作时就不需要同时为每个 service/module/component 开个窗口;
  2. Maven/Gradle 对多 module 支持,这是在编程、构建、测试、部署时可以作为一个“强”结合,可以对 service/module/component 的编程、构建、测试、部署统一配置(DRY);
  3. 使用 git submodule 各个 service/module/component 在编程、构建、测试、部署时是可以独立的,但是也作为一个完整系统部署,测试
  4. 使用 git subtree
  5. 使用 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 开发流程
·2949 字·6 分钟
开发 git

经历了好几个公司和开发团队,git 的使用流程依旧不清晰,这里重点是采用最多的 feature 开发实战流程

进行开发,但 serviceC 的改变每次需要做 两种 commit/push,一次是针对 serviceC (myapp/serviceC/.git 以及 serviceC_remote_repo),另一次是针对 myapp (myapp/.git/modules/serviceC),但这种方式不推荐(说明在下面)。

同样如果另一开发人员对 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 开发流程
·2949 字·6 分钟
开发 git

经历了好几个公司和开发团队,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