浅析 Git 子模块

2021-02-08 04:55发布


I. 何为 Git 子模块?

1.1 - 现状和问题

以前端项目为例,通常我们用 npm dependencies 来集成第三方库,或者将自己维护的多个项目中通用的组件抽取出来。

"devDependencies": {
    "babel-eslint": "^8.2.3",
    "base64-img": "^1.0.3",
    "body-parser": "^1.17.2",
    "colors": "^1.3.0",
    "eslint": "^4.19.1",
    "eslint-plugin-babel": "^5.1.0",
    "express": "^4.15.3",
    "fs-extra": "^3.0.1",
    "fs-watch-tree": "^0.2.5",
    "klaw-sync": "^2.1.0",
    "less": "^2.7.2",
    "lodash": "^4.17.4",
    "node-file-eval": "^1.0.0",
    "nodemon": "^1.11.0",
    "postcss": "^6.0.5",
    "precommit-hook": "^3.0.0",
    "ws": "^5.1.1"
  }

这种方式简单方便、支持广泛,适用于大部分情况;但是对于其中某些库来说,也存在一些痛点

  • 需要第三方库编译打包完成,并发布到 npm

  • 如果第三方库有多个编译选项,则组合多个编译选项,分别打包管理,也是一个繁琐的工作

  • 简单方便,但不够灵活。如果是一个庞大的第三方库,即使你只想使用其中的一个小模块,也得把它整个的下载集成

  • 如果第三方库有了更新,需要更新其版本,并验证项目中对其的依赖配置

  • 如果想看看源码,需要手动去 node_modules 中查找

那么,基于以上几点,如果不得不将第三方源码手动拷贝到项目中,又会带来更多的问题:

  • 第三方库将难以和原库保持同步更新

  • 如果对第三方库做出了较通用的更改和补丁等,无法发布到原库中为其他人所用

  • 对第三方库做出的修改,其 git commits 混杂提交到主项目中,难以单独清晰的管理

一个虽然不一定是最好的,但可行的办法是:

1.2 - Git 中的 submodule

子模块(submodule)允许你将一个 Git 仓库作为另一个 Git 仓库的子目录;
它能让你将另一个仓库克隆到自己的项目中,同时还保持提交的独立

简单的说,子模块的解决方案更像是上面两种的融合,类似于一种特区模式:代码既存在于主项目的子文件夹中,可以灵活的为我所用;在 Git 层面又是独立提交管理的,和主项目的 commit 时间线保持了完全的独立性。

如果第三方库发生了变化,那么项目中的子模块可以自由自主的选择 合并、变基、切换分支 等各种操作。比如一个通用组件作为子模块分别被公司中不同项目引用,则各个项目组做出的改进,最后都可以汇总到主组件库中,为大家所共享。

II. 如何用起子模块?

2.1 - 添加子模块

在当前项目中,添加已有的第三方库:

git submodule add 3RD_LIB_GIT_PATH

默认情况下,子模块会自动放入一个与其仓库同名的子目录中;在末尾也可以加一个自定义的路径参数。

同时项目中会出现一个新的 .gitmodules 配置文件,保存了一些映射关系:

[submodule "3RD_LIB_NAME"]
    path = 3RD_LIB_NAME
    url = 3RD_LIB_GIT_PATH
    
...

...

子模块所在的子目录是被 Git 特殊对待的 – 也就是说,当你不在此目录中时,Git 默认并不跟踪其中的内容,而是将其变动当成一种特殊的提交对待。

2.2 - 克隆含有子模块的项目

克隆含有子模块的项目时,对应的子目录其实默认是空的,需要额外的步骤。

默认做法是:

# 克隆主项目
git clone MAIN_PROJECT_GIT

# 初始化本地配置文件
git submodule init

# 抓取所有数据并检出父项目中列出的合适的提交
git submodule update

更简单一些的做法是在 clone 时加上参数:

git clone --recursive MAIN_PROJECT_GIT

2.3 - 拉取上游变更

在项目中使用子模块的最简单模式,就是只对其更新并享用最新版本,但并不修改之。

更新子模块的命令为:

git submodule update --remote

Git 默认会尝试更新所有子模块;如果子模块数量众多,也可以在以上命令中传入需要更新的子模块名称。

2.4 - 使用子模块

默认情况下,子模块并没有本地分支,而是会停留在一种特殊的 “detached HEAD” 模式下;要对其修改并被 Git 跟踪的话,就要先手动检出分支:

# 检出一个叫 stable 的分支
git checkout stable

然后从上游拉取新的内容,此时有两种选择:

# 选择A:合并
git submodule update --remote --merge

# 选择B:变基
git submodule update --remote --rebase

2.5 - 发布子模块变更

因为主项目并不会跟踪子模块中的变更,也就是说子目录中更改的具体业务文件不会在 push 时被自动发布;所以需要要求 Git 在推送主项目之前检查所有子模块是否已正确提交:

git push --recurse-submodule=check

根据上述检查结果,可以进入每个子模块并手动提交。

还有更简单的做法是自动完成这项操作:

git push --recurse-submodule=on-demand

此时会先推送子模块再推送主项目,如果前者失败整个流程将停止。

2.6 - 解决子模块冲突

会遇到和其他人先后改动了同一个子模块的情况,也就是一个提交是另一个的直接祖先,那么 Git 会简单地选择之后的提交来合并,这样没什么问题。

不过,当两边同时修改,也就是子模块提交已经分叉的情况下,如果尝试合并,Git 会报 “merge following commits not found” 错误。

解决的方法有些麻烦,罗列如下:

# 得到试图合并的两个分支中记录的提交的 SHA-1 值
$ git diff
diff --cc 3RD_LIB_GIT_PATH
index eb41d76,c771610..0000000
--- a/3RD_LIB_GIT_PATH
+++ b/3RD_LIB_GIT_PATH

# 进入子模块目录
$ cd 3RD_LIB_GIT_PATH

# 基于 git diff 的第二个 SHA 创建一个分支
$ git branch my-try-merge-branch c771610
(3RD_LIB_GIT_PATH) $ git merge my-try-merge-branch
Auto-merging src/main.c
CONFLICT (content): Merge conflict in src/main.c
Recorded preimage for 'src/main.c'
Automatic merge failed; fix conflicts and then commit the result.

# 手动解决冲突
$ vim src/main.c

# 返回到主项目目录中
$ cd ..

# 再次检查 SHA-1 值
$ git diff

# 添加解决后的子模块记录
$ git add 3RD_LIB_GIT_PATH

# 提交合并
$ git commit -m "Merge Tom's Changes"

2.7 - 删除子模块

  • 从 .gitmodules 文件中删除相关的行

  • 从 .git/config 中删除相关部分

  • 运行 git rm –cached <子模块名称>

  • 删除 untracked 的子模块文件

III. 子模块有何问题?

  • 需要手动更新子模块代码

  • 第三方库频繁更新时,本项目的 git log 里会生成很多日志

  • 在项目中运行 git status,顶多只能知道子模块有变化,但具体是什么还要到子目录中再去运行一次

  • 正如前面看到的,建立、删除、合并和解决冲突都比较麻烦

IV. 子模块的进化

git subtree 命令,从 git v1.8 后可用,官方推荐使用 subtree 代替 submodule,其并不需要保存 .submodule 这样的元信息。

subtree 用法如下:

4.1 - 第一次添加子目录,建立与 git 项目的关联

# 其中-f意思是在添加远程仓库之后,立即执行fetch
git remote add -f <子仓库名> <子仓库地址>

# --squash意思是把subtree的改动合并成一次commit,这样就不用拉取子项目完整的历史记录。--prefix之后的=等号也可以用空格
git subtree add --prefix=<子目录名> <子仓库名> <分支> --squash

4.2 - 从远程仓库更新子目录

git fetch <远程仓库名> <分支>

git subtree pull --prefix=<子目录名> <远程分支> <分支> --squash

4.3 - 从子目录push到远程仓库

# 需要确认有写权限
git subtree push --prefix=<子目录名> <远程分支名> 分支

V. 总结

  • 子模块适用于需要修改第三方库,或只引用其一部分的场景

  • 子模块能让另一个仓库作为主项目的子目录,同时还保持提交的独立

  • 子模块的若干操作都比较繁琐

  • 应该逐渐用 subtree 代替 submodule 命令,管理、更新都更加方便

VI. 参考资料:

  • https://git-scm.com/book/zh/v2/Git-工具-子模块

  • https://www.atlassian.com/blog/git/git-submodules-workflows-tips

  • https://news.ycombinator.com/item?id=3904932

  • http://slopjong.de/2013/06/04/git-why-submodules-are-evil/

  • https://blog.csdn.net/mountains2001/article/details/72638009

  • https://item.jd.com/12191481.html

  • https://www.slideshare.net/ssusera62527/submodule-subtree

  • https://yihui.name/cn/2017/03/git-submodule/


(end)



—————————————-


转载请注明出处 


长按二维码或搜索 fewelife 关注我们哦


本文分享自微信公众号 - 云前端(fewelife)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

标签: