前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >浅析 Git 子模块

浅析 Git 子模块

作者头像
江米小枣
发布2020-06-15 21:54:07
1.3K0
发布2020-06-15 21:54:07
举报
文章被收录于专栏:云前端云前端

I. 何为 Git 子模块?

1.1 - 现状和问题

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

代码语言:javascript
复制
"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 - 添加子模块

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

代码语言:javascript
复制
git submodule add 3RD_LIB_GIT_PATH

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

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

代码语言:javascript
复制
[submodule "3RD_LIB_NAME"]
   path = 3RD_LIB_NAME
   url = 3RD_LIB_GIT_PATH
   
......

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

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

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

默认做法是:

代码语言:javascript
复制
# 克隆主项目
git clone MAIN_PROJECT_GIT

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

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

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

代码语言:javascript
复制
git clone --recursive MAIN_PROJECT_GIT

2.3 - 拉取上游变更

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

更新子模块的命令为:

代码语言:javascript
复制
git submodule update --remote

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

2.4 - 使用子模块

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

代码语言:javascript
复制
# 检出一个叫 stable 的分支
git checkout stable

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

代码语言:javascript
复制
# 选择A:合并
git submodule update --remote --merge

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

2.5 - 发布子模块变更

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

代码语言:javascript
复制
git push --recurse-submodule=check

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

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

代码语言:javascript
复制
git push --recurse-submodule=on-demand

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

2.6 - 解决子模块冲突

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

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

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

代码语言:javascript
复制
# 得到试图合并的两个分支中记录的提交的 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 项目的关联

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

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

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

代码语言:javascript
复制
git fetch <远程仓库名> <分支>

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

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

代码语言:javascript
复制
# 需要确认有写权限
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/
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-07-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 云前端 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.1 - 现状和问题
  • 1.2 - Git 中的 submodule
  • II. 如何用起子模块?
    • 2.1 - 添加子模块
      • 2.2 - 克隆含有子模块的项目
        • 2.3 - 拉取上游变更
          • 2.4 - 使用子模块
            • 2.5 - 发布子模块变更
              • 2.6 - 解决子模块冲突
                • 2.7 - 删除子模块
                • III. 子模块有何问题?
                • IV. 子模块的进化
                  • 4.1 - 第一次添加子目录,建立与 git 项目的关联
                    • 4.2 - 从远程仓库更新子目录
                      • 4.3 - 从子目录push到远程仓库
                      • V. 总结
                      • VI. 参考资料:
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档