沐光

记录在前端之路的点点滴滴

使用 git submodule 来管理子模块

前言

近期在考虑项目拆分的过程中,对于公共组件的维护总共找到了两种方式,分别是:发布 npm 包与 git submodule 子模块管理。对于 npm 发包的方法这里我就不做赘述了(先前有写到过),而另一种 git submodule 的方法这里就详细介绍一下,对于不想使用发包的方式(比较繁琐)来维护子项目的 coder 来说,这种方式可以算是最为方便的了。

使用场景

有种情况我们经常会遇到:某个工作中的项目需要包含并使用另一个项目。也许是第三方库,或者你独立开发的,用于多个父项目的库。现在问题来了:你想要把它们当做两个独立的项目,同时又想在一个项目中使用另一个。

我们举一个例子。假设你正在开发一个网站然后创建了 Atom 订阅。你决定使用一个库,而不是写自己的 Atom 生成代码。你可能不得不通过 CPAN 安装或 Ruby gem 来包含共享库中的代码,或者将源代码直接拷贝到自己的项目中。如果将这个库包含进来,那么无论用何种方式都很难定制它,部署则更加困难,因为你必须确保每一个客户端都包含该库。如果将代码复制到自己的项目中,那么你做的任何自定义修改都会使合并上游的改动变得困难。

Git 通过子模块来解决这个问题。子模块允许你将一个 Git 仓库作为另一个 Git 仓库的子目录。它能让你将另一个仓库克隆到自己的项目中,同时还保持提交的独立。

该部分直接引用官方的例子,就不造轮子了。

命令使用

使用 git submodule 时,需要的一些命令有:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 当前项目文件名如不指定,则默认为仓库名
# 目标仓库路径可为远程仓库地址,也可为当前相对路径
git submodule add <目标仓库路径> <当前项目文件名>
# 查看子模块状态
git submodule (status)
# 将子模块与远程保持同步(会切换至一个临时分支)
git submodule update --remote
# 将所有子模块都切换至 master 分支
git submodule foreach 'git checkout master'
# 将所有子模块同步远程更新
git submodule foreach 'git pull'
# 查看子模块追踪的内容信息
git diff --cached --submodule

初次添加仓库操作

作为添加者来说,子模块的添加是很简单的一件事,主要步骤如下:

1
2
3
4
5
6
7
8
9
10
11
# 添加仓库
git submodule add <子模块仓库路径> <当前项目文件名>

# 此时打印 git status 会显示 submodule 的一些信息
# .submodule 文件内存储的就是我们需要的映射信息了

# 提交更新并推送至远程
# 当仓库提交至远程时,子模块的内容并不会被带过去
git add .
git commit -m "init submodule"
git push

拷贝远程仓库

拷贝远程带有子模块的项目需要稍微注意一点,那就是子模块需要额外的步骤才能将内容拷贝下来,具体步骤如下:

1
2
3
4
5
6
7
8
9
# 克隆仓库
git clone <远程仓库地址>
# 此时子模块是没有内容的,所以
# 初始化本地配置文件
git submodule init
# 配置文件有记录对应的 commit id,此将子模块文件同步至配置文件内记录的 id 处
# 注意: `git submodule update` 拿到的文件内容并不一定与子模块仓库的最新状态保持一致
# 因为它记录的为先前初始化时的版本,需要额外更新
git submodule update

三步简化成一步的操作为: git clone <远程仓库地址> –recursive

更新子模块

更新的方法也有多种,最为基础的处理方式是:

1
2
3
4
5
6
7
# 进入项目中的子模块内
cd <child-module>
# 拉取新内容
git fetch
# 合并新内容
git merge
# fetch 和 merge 可以简化为一步: git pull

当然,此做法比较繁杂,较为简便的处理方法是(无需进入子模块):

1
2
# 添加 remote 会更新至远程项目的最新版本,否则是本地最新版本
git submodule update --remote

Git 会自动进入子模块并更新它。但是该命令会默认假定你想要更新并检出子模块的 master 分支。简单的说,此时子模块会自动生成一个新临时分支,与远程的源保持一致,如果子模块内部有改动,想要迁回,那么将子模块的 branch 切回原来的分支即可。

例如:子模块本来是 master 分支,后来远程子模块的源更新了,此时 git submodule update --remote 会将子模块分支切换成一个 hash id 分支。此时进入子模块目录,git checkout master 切回 master 分支,你会发现该 master 分支内容与执行 update 指令之前的是一致的,没有变动,但会提醒你远程的源已经更新了,需要手动 pull 一下。

这样看来,这两种写法都没差啦~反正最后都得自己手动处理一下。

对于包含多个子模块的项目来说,进入子模块一个个更新着实是很麻烦,批量更新可以使用 foreach,例如:

1
2
3
4
5
# 同步最新的 head 指针,否则
git submodule update --remote
# 将默认迁出的分支迁回 master,然后拉取更新
git submodule foreach git checkout master
git submodule foreach git pull

或者,如果知道更新了,可以省略掉前两步骤(前面步骤主要是为了 git status | git submodules 看有没有子模块变动),直接拉取就行,这样也不用再去切分支了。

1
git submodule foreach git pull

更改子模块

前面对子模块的处理仅仅只是同步更新而已,这和使用 npm 包没啥区别,但毕竟是将子模块给弄进项目了,当个包放在那里用,那也太委屈了。其实在主项目内,我们还可以对子模块进行更新,还可以并发布其改动,这样就能节省很多时间和劳力了(前提是别改造得别人无法使用,变成私有代码库了)。

这里仅介绍比较省时的操作,直接启动 merge ,我们可以用下面这个命令:

1
git submodule update --remote --merge

进入子模块后,我们就能和平常一样的合并冲突了。

在主分支进行推送时,我们得保证当前分支的所有子模块都已经与远程一致(或者都落后,但不可 diverged),防止别的伙伴接受不到新的变动,因此推送的命令改为:

1
2
3
# 这样 git 会进入到子模块中然后在推送主项目前推送了它。如果那个子模块因为某些原因推送失败,主项目也会推送失败
# 该方法暂未尝试,一直用的 foreach push,比较方便
git push --recurse-submodules=on-demand

注:如果出现了无法推送的情况,特别是远程分支为本地项目的情况,此时我们需要进入该远程分支,然后设置 ‘.git/config’ 文件,加上内容如下:

1
2
[receive]
denyCurrentBranch = false

这样就能 push 了。

删除子模块

删除子模块需要进行 2 步操作,首先得删除对应的 cache 缓存追踪,然后再删除对应的模块文件

1
2
git rm --cached <fileName>
rm --rf <fileName>

参考文章