背景
最近接手参与了一个大型且很复杂的低代码项目,里面模块依赖众多,管理起来遇到一些麻烦,从代码复用和开发版本控制来看,有两个方面需要进行管理或改进:
- 有些依赖是从开源项目上复制过来。其代码在主项目中,和其他业务代码除了目录区别外,已基本融合进了主项目,无法进行版本控制,在不断的迭代过程中,随着业务代码的加入,耦合性越来越高,独立性逐渐降低。对后续迁移,独立出去造成一定困难;
- 项目中部分模块,单独抽离出去后,独立成单个工程。但是没有使用 npm package 的方式管理,原因是当前仍处于开发阶段,不断需要更新升级。利用版本号管理,将出现大量版本号,无法确保对应关系的管理。而是使用项目编译后的文件,替换掉主项目对应引用的文件。该方法操作简单,不过要控制好项目分支,其代码复用起来困难,编译文件有冗余,造成主项目最终编译文件过大。
而这两个麻烦,主要是没有使用子项目管理的形式进行管理。当前比较常用的,利用 Git 命令实现的两种方式分别是 git submodule 和 git subtree 。这两种方式各有优劣势,需要我们依据具体项目情况和诉求,选择合适的方式进行。
Git submodule
Git submodule(子模块)是Git版本控制系统中的一种机制,主要用于管理项目的依赖关系。
【重点】该方式是管理依赖关系!
Git submodule 的优势
该方式是 Git 推出的最早的子项目管理机制。其优势是:
- 依赖管理方式:子项目作为一个独立的项目被主项目引入,并在主项目根目录下生成一个
.gitmodules
的文件。在该文件中定义了子模块的路径、分支等信息。 - 独立开发:相对主项目,可以独立进行开发、维护和版本控制。每个子模块有各自的提交记录和分支,相互不影响。
- 版本控制:有自己的提交历史,可以针对特定版本进行管理回滚。
- 适合子项目频繁变动:子项目直接暴露在主项目中,可以在主项目中直接修改,立即调试,方便快捷。
[submodule "src/child01"]
path = src/child01
url = https://gitee.com/weijianxu/child-project-01.git
branch = dev
Git submodule 的劣势
虽然它的功能很强大,但是其缺点也比较明显:
- 初始化和更新复杂:使用子模块,需要执行额外的初始化和更新命令。在克隆包含子模块的项目时,需要特别注意子模块的初始化和更新操作。这会增加一些额外的步骤和复杂性,特别是对于不熟悉Git submodule的开发者来说。
- 仓库复杂度:子项目直接可以单独管理,一定程度增加了仓库的复杂度。
适用的场景
基于上述优劣势,可以分析得出Git submodule 方式适用的场景为:
- 子模块需要独立开发和维护的场景;
- 子项目需要频繁在主项目中进行修改、调试、迭代;
一些问题
- 频繁变更时,拉取别人代码,总是会有一条 submodule 的合并记录需要提交;
- 变更了子项目分支,不小心提交了,导致其他人编译失败。需要保证推送时是完整,且在设置好的分支上。自己开发时,可以切到自己的分支,但是需要切换到默认分支,并将代码合并过来。
- 分支变成了git提交记录某一hash值。原因:出现这种情况,可能是频繁变更,本地和远程信息不同步导致。解决方案:切换到正确版本即可(切换前请先用 git stash保存本地变更)。
Git subtree
Git subtree是Git版本控制系统中的一种机制,用于将一个Git仓库的特定目录作为另一个Git仓库的子目录。
【重点】主项目的一个特定目录!
Git subtree 的优势
该方式是 Git 目前比较新的子项目管理机制。其优势是:
- 管理简化:子项目在主项目中与其他代码无多大差异,可进行任意修改。
- 初始化和更新相对容易:子项目作为主项目的一部分,被直接管理和更新,简化了流程。说直白点就是:子项目加入后,其他开发拉去推送,和其他普通代码文件无异。
- 提交历史完整:保留了子项目的完成记录,便于查看和追踪代码。
Git subtree 的劣势
上述优势,有时又是它的劣势:
- 耦合性高:子项目不独立,难区分。需要单独提交,才能保证子项目上有新的代码。
- 分支管理困难:不能很方便的切换分支。若要回滚,很麻烦,因为提交记录和主项目其他提交混在一起。
- 历史记录一直存在:当不再使用该子项目,删除后,其提交记录仍然在主项目中。当将一个子项目使用 subtree 方式合并过去,其历史记录也被合并过去,导致历史记录膨胀(待定)。
- 更新复杂:子项目更新命令操作繁杂。
适用的场景
基于上述优劣势,可以分析得出Git subtree 方式适用的场景为:
- 需要将外部仓库的特定部分集成到主项目中。
- 适用不需要独立开发,或者对分支要求不多的场景。换句话说,子项目需要运行在主项目中。
- 子项目功能单一,仍变动较多,尚未达到可以使用 npm package 管理的时候。
一些问题
- 提交修改时查找花费很长时间:主要原因是本地很久没有进行split操作,且这段时间内提交记录很大,导致检索出子项目这段时间内的变更,花费太长时间。因此需要定期执行下split操作。
- 执行split操作后,检索仍然检索之前的变更。原因是:执行 split 操作时,最后一次提交记录是 merge 之类的合并分支操作,导致检索时,两个分支都会查找,从而失效。因此执行 split 时,确认最后一次不是 merge 之类的操作。
实践
两种方式各有优缺点,需要根据场景合理使用。也可以混合使用。下面介绍各自的最佳实践步骤。
Git submodule 最佳实践
- 向主项目中添加子项目(只需要加一次即可,若不加分支,默认是主干分支master或main):
git submodule add -b <分支名> <仓库URL> <本地目录>
- 主项目中从远程仓库更新子项目:
# --remote 建议带上该参数不可少,表示从从远程仓库拉取最新代码
git submodule update --remote
# 多个子项目同时存在,同时更新
git submodule foreach git pull
- 在主项目中,修改了子项目的内容,建议和主项目的内容分开提交。
- 克隆或者拉取带子项目的主项目(只需执行一次即可):
git submodule init # 初始化
git submodule update # 拉取最新代码
vscode会将子项目单独管理,可以对其进行git常规操作,避免命令输入的麻烦,操作方式如下所示。
- 拉取远程仓库修改:
- 主项目修改子项目后提交
【特别注意】: 修改产生的 commit 需要提交上,避免产生不必要的冲突。
Git subtree 最佳实践
- 向主项目中添加子项目(只需要加一次即可,其他人只需拉取即可):
git subtree add --prefix=<本地目录> <仓库URL> <分支>
- 主项目中从远程仓库更新子项目:
git subtree pull --prefix=<本地目录> <仓库URL|仓库别名> <分支> --squash
为了简化提交命令,可以用git remote add <仓库别名> <仓库URL>
来简化命令。
使用 git remote -v
可以查看添加情况
其中--squash
参数表示不拉取全部历史信息(距离上一次pull),若设置了该参数,下面的 --rejoin
就不需要了。
- 在主项目中,修改了子项目的内容,更新比较繁琐。
- 使用 push 命令提交。
git subtree push --prefix=<本地目录> <仓库URL|仓库别名> <分支>
- 然后,切分起点:在进行代码修改完成,建议先使用 split 命令为子仓库切分出起点,避免下次push时,从头开始检索,导致提交速度变慢。
git subtree split [--rejoin] --prefix=<本地目录> --branch <子项目的分支名>
- 当拉取他人设置了subtree的仓库或者提交记录后,提交完本地变更后,使用一次 split 命令。避免下次出现上述问题。
在 vscode 中尚未提供良好的操作管理交互,不过可以通过使用 git 命令别名来简化。例如:
# pull
git config alias.stpl 'subtree pull --prefix=<本地目录> <仓库URL|仓库别名> <分支> --squash'
# push
git config alias.stps 'subtree push --prefix=<本地目录> <仓库URL|仓库别名> <分支>'
# split
git config alias.stsp 'subtree split [--rejoin] --prefix=<本地目录> --branch <子项目的分支名>'
[参考资料]