简单记录一下 git submodule 的用法,注意不是 subtree。

添加子模块

可以使用 submodule 功能添加子模块仓库

1
git submodule add <子模块仓库的url>

默认会在主仓库下创建一个与子模块仓库同名的文件夹,用于存储子模块的所有内容。 也可以指定子模块使用的文件夹

1
git submodule add <子模块仓库的url> subdir

子模块所使用的仓库url最好是公开可访问的,并且最好没有修改推送权限,只能单向接受远程更新的推送,这样最省事。

克隆含子模块的仓库

在克隆一个含有子模块的git仓库时,默认对主仓库的 git clone 命令不会把子模块也拉取下拉,只会得到一个包含子模块信息的.gitsubmodules文件和子模块对应的空目录,这是考虑到实际用途中某些子模块可能是可选项而非必选项。

可以在主仓库中使用下面的命令进行子模块的初始化和更新

1
2
git submodule init
git submodule update

这两个命令可以合起来

1
git submodule update --init

如果存在多个子模块,上述命令默认对所有子模块进行,但是我们也可以指定只对某个子模块执行操作,例如

1
2
3
4
git submodule init <submodule的文件夹的相对路径>
git submodule update <submodule的文件夹的相对路径>
# or
git submodule update --init <submodule的文件夹的相对路径>

当然,如果已知主仓库中存在子模块,并且全部需要拉取下拉,直接在git clone 命令中加上如下选项即可

1
git clone --recurse-submodules <主项目Git仓库地址>

子模块底层逻辑

一个被视作子模块的git仓库并不知道它被视作子仓库,这种父子关系只由主仓库单向维护。

在子模块的远程仓库中不会体现主仓库的任何信息,但是在本地有一些区别: 子模块不会使用单独的 .git/ 文件夹存储数据,而是会共用主仓库的 .git/ 文件夹,在其中新建一个子模块的对应目录。 具体实现是:子模块目录下存储一个特殊的 .git 文本文件,其中的内容形如

1
gitdir: ../.git/modules/subdemo

也就是指向主仓库.git/文件下的对应目录。

git 对于含子模块的仓库,主要通过一个特殊的 .gitsubmodule 文本文件来管理,其中记录了所有子模块的核心信息(名称,路径,url,可能还有分支信息),内容形如

1
2
3
[submodule "subdemo"]
path = subdemo
url = git@github.com:xxx/subdemo.git

这个文件需要被主仓库纳入正常的版本管理中。

主仓库其实也不会过多关注子模块的细节,它所知道的除了 .gitsubmodule 中的信息,还有当前子模块中所处的版本节点信息,以及现在的子模块中是否存在更改,仅此而已。在主模块的远程仓库中,对应子模块的子目录其实只是一个链接,指向子模块远程仓库中的某个提交版本,并没有冗余地存储子模块的数据。

在子模块之外的目录中执行 git 命令,默认都是对主仓库进行的,并不会过多关注子模块的细节,只是显示子模块的整体状态(除了 git submodule xxx形式的命令)。在子模块的目录中执行的git命令则是对子模块仓库进行的。

在子模块之外的目录中通过 git status 查看,会发现 git 对子模块所在的整个目录有特殊处理,并不会关注其中的细节。

更新子模块

更新子模块的方式其实很简单:直接进入子模块对应的目录,此时所有的git命令都是对子模块进行的,正常在本地修改,然后提交推送,或者直接拉取远程的更新即可。

在本地主动更新子模块其实是不太合理的:主仓库作为子模块的使用者,对于子模块保持只读状态即可,不应该越俎代庖地负责对子模块主动更新,只要被动地接受远程仓库的更新即可。

由于主仓库记录了子仓库对应的提交版本,一旦子仓库发生任何版本更新,主仓库中就会显示子模块进行了更改(但是默认不显示细节),我们也需要更新主仓库,让它记录最新的子仓库版本。

同样的逻辑,如果远程的主仓库进行了更新,它所记录的子模块版本可能与本地不同,在本地主仓库执行git pull时也会显示出来,但是这只是主仓库所记录的子模块版本号的变动,主仓库不会主动为我们拉取子模块的更新,只会显示存在差异。

除此之外,git submodules命令还支持在主仓库中直接对子仓库拉取远程的更新,例如

1
git submodule update --remote

但是这种操作很麻烦,它似乎会主动追踪master分支,要进行相应的配置或者加上--rebase选项等,否则很容易导致子模块仓库的HEAD在更新之后处于游离状态,还是直接进入子目录处理比较安全省事。

必须谨慎处理主仓库和子模块仓库的更新和远程同步,否则可能出现错误,最好保持子模块的版本稳定,不要对其频繁更新。

移除子模块

删除子模块的步骤比较复杂,需要处理各个地方残留的配置,至少包括如下步骤:(可能还有残留)

  • rm -rf subdir/ 在文件系统中删除子模块目录及源码
  • vim .gitmodules 删除项目目录下.gitmodules 文件中对应子模块的相关条目
  • vim .git/config 删除仓库级配置文件中子模块相关条目
  • rm .git/modules/xxx 删除.git/中的子模块对应目录

然后将修改提交即可。