Git submodule 与 subtree 子项目管理优劣与最佳实践分析

190 阅读8分钟

背景

最近接手参与了一个大型且很复杂的低代码项目,里面模块依赖众多,管理起来遇到一些麻烦,从代码复用和开发版本控制来看,有两个方面需要进行管理或改进:

  1. 有些依赖是从开源项目上复制过来。其代码在主项目中,和其他业务代码除了目录区别外,已基本融合进了主项目,无法进行版本控制,在不断的迭代过程中,随着业务代码的加入,耦合性越来越高,独立性逐渐降低。对后续迁移,独立出去造成一定困难;
  2. 项目中部分模块,单独抽离出去后,独立成单个工程。但是没有使用 npm package 的方式管理,原因是当前仍处于开发阶段,不断需要更新升级。利用版本号管理,将出现大量版本号,无法确保对应关系的管理。而是使用项目编译后的文件,替换掉主项目对应引用的文件。该方法操作简单,不过要控制好项目分支,其代码复用起来困难,编译文件有冗余,造成主项目最终编译文件过大。

而这两个麻烦,主要是没有使用子项目管理的形式进行管理。当前比较常用的,利用 Git 命令实现的两种方式分别是 git submodule 和 git subtree 。这两种方式各有优劣势,需要我们依据具体项目情况和诉求,选择合适的方式进行。

Git submodule

Git submodule(子模块)是Git版本控制系统中的一种机制,主要用于管理项目的依赖关系。

【重点】该方式是管理依赖关系!

Git submodule 的优势

该方式是 Git 推出的最早的子项目管理机制。其优势是:

  1. 依赖管理方式:子项目作为一个独立的项目被主项目引入,并在主项目根目录下生成一个 .gitmodules 的文件。在该文件中定义了子模块的路径、分支等信息。
  2. 独立开发:相对主项目,可以独立进行开发、维护和版本控制。每个子模块有各自的提交记录和分支,相互不影响。
  3. 版本控制:有自己的提交历史,可以针对特定版本进行管理回滚。
  4. 适合子项目频繁变动:子项目直接暴露在主项目中,可以在主项目中直接修改,立即调试,方便快捷。
[submodule "src/child01"]
  path = src/child01
  url = https://gitee.com/weijianxu/child-project-01.git
  branch = dev

Git submodule 的劣势

虽然它的功能很强大,但是其缺点也比较明显:

  1. 初始化和更新复杂:使用子模块,需要执行额外的初始化和更新命令。在克隆包含子模块的项目时,需要特别注意子模块的初始化和更新操作。这会增加一些额外的步骤和复杂性,特别是对于不熟悉Git submodule的开发者来说。
  2. 仓库复杂度:子项目直接可以单独管理,一定程度增加了仓库的复杂度。

适用的场景

基于上述优劣势,可以分析得出Git submodule 方式适用的场景为:

  1. 子模块需要独立开发和维护的场景;
  2. 子项目需要频繁在主项目中进行修改、调试、迭代;

一些问题

  1. 频繁变更时,拉取别人代码,总是会有一条 submodule 的合并记录需要提交;
  2. 变更了子项目分支,不小心提交了,导致其他人编译失败。需要保证推送时是完整,且在设置好的分支上。自己开发时,可以切到自己的分支,但是需要切换到默认分支,并将代码合并过来。
  3. 分支变成了git提交记录某一hash值。原因:出现这种情况,可能是频繁变更,本地和远程信息不同步导致。解决方案:切换到正确版本即可(切换前请先用 git stash保存本地变更)。

Git subtree

Git subtree是Git版本控制系统中的一种机制,用于将一个Git仓库的特定目录作为另一个Git仓库的子目录。

【重点】主项目的一个特定目录!

Git subtree 的优势

该方式是 Git 目前比较新的子项目管理机制。其优势是:

  1. 管理简化:子项目在主项目中与其他代码无多大差异,可进行任意修改。
  2. 初始化和更新相对容易:子项目作为主项目的一部分,被直接管理和更新,简化了流程。说直白点就是:子项目加入后,其他开发拉去推送,和其他普通代码文件无异。
  3. 提交历史完整:保留了子项目的完成记录,便于查看和追踪代码。

Git subtree 的劣势

上述优势,有时又是它的劣势:

  1. 耦合性高:子项目不独立,难区分。需要单独提交,才能保证子项目上有新的代码。
  2. 分支管理困难:不能很方便的切换分支。若要回滚,很麻烦,因为提交记录和主项目其他提交混在一起。
  3. 历史记录一直存在:当不再使用该子项目,删除后,其提交记录仍然在主项目中。当将一个子项目使用 subtree 方式合并过去,其历史记录也被合并过去,导致历史记录膨胀(待定)
  4. 更新复杂:子项目更新命令操作繁杂。

适用的场景

基于上述优劣势,可以分析得出Git subtree 方式适用的场景为:

  1. 需要将外部仓库的特定部分集成到主项目中。
  2. 适用不需要独立开发,或者对分支要求不多的场景。换句话说,子项目需要运行在主项目中。
  3. 子项目功能单一,仍变动较多,尚未达到可以使用 npm package 管理的时候。

一些问题

  1. 提交修改时查找花费很长时间:主要原因是本地很久没有进行split操作,且这段时间内提交记录很大,导致检索出子项目这段时间内的变更,花费太长时间。因此需要定期执行下split操作。
  2. 执行split操作后,检索仍然检索之前的变更。原因是:执行 split 操作时,最后一次提交记录是 merge 之类的合并分支操作,导致检索时,两个分支都会查找,从而失效。因此执行 split 时,确认最后一次不是 merge 之类的操作。

实践

两种方式各有优缺点,需要根据场景合理使用。也可以混合使用。下面介绍各自的最佳实践步骤。

Git submodule 最佳实践

  1. 向主项目中添加子项目(只需要加一次即可,若不加分支,默认是主干分支master或main):
git submodule add -b <分支名> <仓库URL> <本地目录>
  1. 主项目中从远程仓库更新子项目:
# --remote 建议带上该参数不可少,表示从从远程仓库拉取最新代码
git submodule update --remote
# 多个子项目同时存在,同时更新
git submodule foreach git pull
  1. 在主项目中,修改了子项目的内容,建议和主项目的内容分开提交
  2. 克隆或者拉取带子项目的主项目(只需执行一次即可):
git submodule init # 初始化
git submodule update # 拉取最新代码

vscode会将子项目单独管理,可以对其进行git常规操作,避免命令输入的麻烦,操作方式如下所示。

  1. 拉取远程仓库修改:

截图1.png

  1. 主项目修改子项目后提交

image.png

【特别注意】: 修改产生的 commit 需要提交上,避免产生不必要的冲突。

Git subtree 最佳实践

  1. 向主项目中添加子项目(只需要加一次即可,其他人只需拉取即可):
git subtree add --prefix=<本地目录> <仓库URL> <分支>
  1. 主项目中从远程仓库更新子项目:
git subtree pull --prefix=<本地目录> <仓库URL|仓库别名> <分支> --squash

为了简化提交命令,可以用git remote add <仓库别名> <仓库URL> 来简化命令。 使用 git remote -v 可以查看添加情况

image.png

其中--squash参数表示不拉取全部历史信息(距离上一次pull),若设置了该参数,下面的 --rejoin 就不需要了

  1. 在主项目中,修改了子项目的内容,更新比较繁琐。
    • 使用 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 <子项目的分支名>'

[参考资料]

  1. Git submodule 和 subtree 的区别
  2. Git - - subtree与submodule
  3. git submodule 完整用法整理 
  4. Git subtree 管理子项目包使用小结
  5. Subtree