一.背景
经常面临一些场景,想要把大代码库(repo)拆分成多个小的repo,例如:

现有代码库体积庞大,且模块管理混乱,经常容易错改别人的东西

某个模块需要单独构建,比如jQuery项目中的React试点、Node项目中的纯前端部分、Electron项目中的UI部分等等

某个模块是黑盒依赖项,开发中仅依赖其构建后的版本,比如框架类库等

针对诸如此类的情况,一般有3种解决方案:

npm package:把依赖项拆出去作为npm package,代码库随之独立出去

monorepo:单repo体积庞大没关系,分模块管理好就行

git submodules:把依赖项拆分到多个独立repo,作为主repo的submodule

npm package
npm package的优势在于成熟的管理依赖机制,规范且易用,缺点是主项目只能通过package版本号获取独立模块的更新,在主项目需要与子模块联调的场景就会非常麻烦:

主项目:调不通啊
子模块:有点问题,我改一下...改版本号-构建-发布npm package
主项目:更新依赖,再试...还是调不通啊
子模块:还有点问题...
频繁发版比较蠢,可以本地修改构建再拷贝一份过去,但还是有些麻烦。当然,通常可以通过mock接口或数据把联调依赖拆解开,但有时候mock全套API成本比较高,而且假的势必没有真的好用

monorepo
monorepo主张不拆分repo,而是在单repo里统一管理各个模块的构建流程、版本号等等,并且鼓励改别人的代码

这在模块边界清晰、owner明确的项目中很合适(如React、Babel等),但实际应用中,业务repo很难保持清晰的模块边界与依赖关系,此时monorepo就变得理想化了

git submodules
git submodules提供了一种类似于npm package的依赖管理机制,包括添加、删除、更新依赖项等功能,区别在于前者所管理的依赖是子模块的源码,后者管理的是子模块的构建产物。在这一点上,git submodules与monorepo一致(都关心子模块的源码)

这样主项目需要与子模块频繁联调时的麻烦就不复存在了,因为主项目拉取到的submodules都是完整repo,可以直接修改-构建-提交

二.submodules与monorepo
从结构上看,submodules项目的主repo与monorepo很像,相当于把monorepo里的各模块抽离到了独立repo,仅记录主repo所依赖的各模块版本号(commit hash形式)

具体的,monorepo在单repo里存放所有子模块源码(packages/xxx/src),例如:

react/  packages/    react-dom/      /src    react-reconciler/      /src    ...

而submodules只在主repo里存放所有子模块“索引”(repo url + branch name + commit hash),例如:

# 主repo的.gitmodules文件[submodule "react-dom"]    path = packages/react-dom    url = https://github.com/facebook/react.git    branch = master[submodule "react-reconciler"]    path = packages/react-reconciler    url = https://github.com/facebook/react.git    branch = master...

主repo里只保留对应的空目录作为子模块的“坑”,并不存其放源码:

react/  packages/    react-dom/        # 空目录    react-reconciler/ # 空目录

拉取所有submodules依赖后,实际目录结构如下:

react/  packages/    react-dom/      /src    react-reconciler/      /src    ...

主repo并不追踪子模块源码,仅记录其版本号(commit hash形式):

# 输出空,表示不追踪子模块src$ git ls-tree -r master | grep packages/react-dom/src# 查看主repo里被git追踪的子模块坑位$ git ls-tree -r master | grep ' commit'160000 commit 3edf340cee50fd4bc918a0a95b438a30447ae042 packages/react-dom160000 commit 373f207b09a7bf900fa82c3188aeefdc9ce6146c packages/react-reconciler...

P.S.git ls-tree的输出格式含义,见Output Format

三.具体用法
git submodule命令用来管理子模块:

$ git submodule --helpgit-submodule - Initialize, update or inspect submodules# 初始化git submodule init# 增git submodule add# 删git submodule deinit# 改(版本控制)git submodule update添加子模块依赖$ cd ./react# 添加依赖$ git submodule add -b master https://github.com/path-to/react-dom.git src/packages/react-dom

会在主repo创建一个src/packages/react-dom空目录,作为子模块的坑位。实际上,add过程主要发生了3件事:

clone一份子模块repo到主repo的git缓存目录里,例如.git/modules/src/packages/react-dom

创建坑位空目录,并把子模块repo的最新commit hash与之关联

在主repo根目录按需创建.gitmodules文件,记录子模块repo地址(url),分支名(branch)以及坑位路径(path)

然后提交这些子模块配置:

$ git add ./src/packages/react-dom ./.gitmodules$ git commit -m "build: add react-dom submodule"$ git push origin master

接下来本地拉取子模块完成初始化:

# 初始化子模块$ git submodule update --init

会把子模块repo clone到src/packages/react-dom目录下,实际发生了2件事:

检查缓存是否存在clone好的子模块repo(比如clone来的主repo并没add过,就不存在缓存),按需clone在子模块repo根目录创建.git/config,记录其repo地址(url)

初始化子模块
在clone含有submodules的repo后,要进行初始化:

# 创建一些本地配置$ git submodule init# 拉取各子模块repo$ git submodule update --init

也可以在clone主repo时,通过--recursive选项也能完成上面两步工作:

$ git clone git://gihub.com/path-to/main-repo.git --recursive

拉取子模块更新
更新所有子模块:

$ git submodule foreach submodule update --remote

只有一个子模块的话,不必foreach:

$ git submodule update --remote

会拉取子模块对应分支的最新代码,如有更新,占位目录的git状态会发生变化:

$ git status

modified:   src/packages/react-dom (new commits)

实际上是commit hash发生了变化:

$ $ git diffdiff --git a/src/packages/react-dom b/src/packages/react-domindex 3edf340cee..d056efbc62 160000--- a/src/packages/react-dom+++ b/src/packages/react-dom@@ -1 +1 @@-Subproject commit 3edf340cee50fd4bc918a0a95b438a30447ae042+Subproject commit d056efbc62cbf976b4ef83e70d7019fba4506e85

P.S.submodules里的commit hash相当于npm package的dependencies版本号

控制依赖项版本
想要更新主repo所依赖的子模块版本的话,提交这个commit hash变更:

$ git add src/packages/react-dom$ git commit -m "build: update react-dom submodule"$ git push origin master

否则不加--remote选项滚回当前依赖版本:

$ git submodule update改动子模块代码

子模块是独立repo,正常操作即可:

$ cd ./packages/react-dom# 注意切分支,通常是detached状态$ git checkout master$ git add .$ git commit -m 'feat: xxx'$ git push origin master

之后,主repo就能通过git submodule update --remote拉取到最新版本,再由主repo决定是否要升级其依赖的子模块版本

P.S.关于submodules的更多用法,见7.11 Git Tools – Submodules

四.常见问题
子模块分支处于detached状态
每次执行git submodule update --remote后,子模块会处于detached状态,例如:

$ cd ./packages/react-dom$ git branch* (HEAD detached at ac4d1fc)  master

设计如此,没有太好的解决办法:

It’s also important to realize that a submodule reference within the host repository is not a reference to a specific branch of that submodule’s project, it points directly to a specific commit (or SHA1 reference), it is not a symbolic reference such as a branch or tag. In technical terms, it’s a detached HEAD pointing directly to the latest commit as of the submodule add.

因此在改动子项目代码之前,需要手动切换到master分支:

$ git checkout master$ git add .$ git commit -m 'feat: xxx'$ git push origin master

本地子模块缓存
当子模块repo发生迁移时,进行git submodule add可能会遇到本地缓存的问题:

$ git submodule add ssh://XXX.XXX.XXX.XXX:XXXXX/opt/git/fdf.git projets/fdfA git directory for 'projets/fdf' is found locally with remote(s): origin ssh://git@XXX.XXX.XXX.XXX:XXXXX/opt/git/fdf.git If you want to reuse this local git directory instead of cloning again from ssh://XXX.XXX.XXX.XXX:XXXXX/opt/git/fdf.git use the '--force' option. If the local git directory is not the correct repo or you are unsure what this means choose another name with the '--name' option.

需要先删掉原配置(第2第3步),再本地缓存的子模块信息(第1第4步):

# 1.删掉git缓存及物理文件$ git rm --cached path_to_submodule$ rm -rf path_to_submodule# 2.删掉.gitmodules里该子模块的相关配置$ vi .gitmodules[submodule "path_to_submodule"]    path = path_to_submodule    url = https://github.com/path_to_submodule# 3.删掉.git/config里该子模块相关配置$ vi .git/config[submodule "path_to_submodule"]    url = https://github.com/path_to_submodule# 4.删掉子模块缓存$ rm -rf .git/modules/path_to_submodule

清理完成之后重新git submodule add即可

P.S.第4步中,子模块的缓存位置可以通过如下命令查看:

$ cat path_to_submodule/.gitgitdir: ../.git/modules/path_to_submodule

P.S.更多常见问题,见Using Git Submodules

参考资料
GETTING GIT SUBMODULE TO TRACK A BRANCH

7.11 Git Tools – Submodules

Using Git Submodules

Git submodule add: “a git directory is found locally” issue

更多相关文章

  1. MyBatis 延迟加载、一二级缓存、架构设计的面试题(常问,重点了解)
  2. 每日学习-ansible yum模块
  3. 帮你解读什么是Redis缓存穿透和缓存雪崩
  4. 每日学习-ansible firewalld模块
  5. MyBatis之Mapper XML 文件详解(六)-缓存配置
  6. Spring【DAO模块】知识要点
  7. Spring【AOP模块】就这么简单
  8. 图书管理系统【用户、购买、订单模块、添加权限】

随机推荐

  1. 图书管理系统【总结】
  2. 购物车案例【简单版】
  3. 【编测编学】接口测试面试题必背(下)
  4. springboot 读写 session 交互参数
  5. AJAX跨域完全讲解
  6. Javascript面向对象入门
  7. DOM编程
  8. Juqery就是这么简单
  9. python入门教程12-03 (python语法入门之进
  10. 客户关系管理系统