Skip to main content

Git 多模块的支持

·794 字·4 分钟
开发 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,可遵循之前介绍的

大型项目 feature 开发 Git 流程
·337 字·2 分钟
开发 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 时遵循之前介绍的

大型项目 feature 开发 Git 流程
·337 字·2 分钟
开发 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"

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 。