Skip to content

git submodule 和 git subtree,你会选择哪个来管理子项目?

前言

如果想在一个项目中用另一个项目的代码,你会怎么做呢?

有同学说,可以发一个 npm 包呀,然后在另一个项目里引入。

这样是可以,但是如果经常需要改动它的源码呢?这样频繁发包就很麻烦。

那可以用 monorepo 的形式来组织呀,也就是一个项目下包含多个包,它们之间可以相互依赖。

这样确实可以频繁改动源码,然后另一个包里就直接可用了。

但如果这个包是一个独立的 git 仓库,我希望它虽然在另一个项目里用了,但要保留 git 仓库的独立性呢?

这种就可以用 git submodule 或者 git subtree 了。这俩都实现了一个 git 项目里引入了另一个 git 项目的功能。

那 submodule 和 subtree 都能做这个,它俩有什么区别呢?我该用哪个好呢?

这篇文章我们就来详细对比下 git submodule 还有 git subtree。

准备环境

首先我们创建一个名为 git-research-child 的文件夹,然后初始化为 git 项目,并提交三次推送到远程,如下:

3 个 commit,每个文件一个 commit。

接着我们创建一个名为 git-research 的项目,并推送到远程。

接着我们需要在 git-research 项目里引入 git-research-child。 该怎么做呢?

submodule

我们先用 git submodule 的方式。

在 git-research 项目里执行:

shell
git submodule add https://gitee.com/luxcurl/git-research-child.git child

这个命令就是将 git-research-child 项目添加到 git-research 项目的 child 目录下:

然后我们再在 child 目录下再添加一个 git submodule,这个 git submodule 就是 git-research-child 项目本身:

shell
cd child
git submodule add https://gitee.com/luxcurl/git-research-child.git child2

现在就是两级 git submodule 了:

在 .gitmodules 里记录着它的 url(submodule 的远程 url)和保存的 path(submodule 的目录路径名):

前面说 submodule 能保留独立性,怎么看出来的呢?

首先,它有独立的 .git 目录,代表是单独 git 项目。

submodule 的 .git 目录其实是放在根 git 项目的 .git 下的:

这样就保证了它们依然可以独立的 pull 和 push。

比如我在 child 里加了一个 444.md 的文件:

然后你会发现 child 目录是可以单独 执行 git add、git commit、git push 的,它依然是独立的项目,父项目只是记录了它关联的 commit id 是啥。

这就是 submodule 的独立性,你可以在这个目录下单独执行 pull、push,单独管理变更,父项目只是记录关联的 commit 的变化。

那如果别人 clone 下这个项目来,还有这个 submodule 么?

我们 clone 下 git-research 项目试试:

shell
git clone https://gitee.com/luxcurl/git-research.git

可以看到确实有 child 这个目录,但是没内容。

这是因为它需要单独初始化一下并更新下代码:

shell
git submodule init
git submodule update

或者执行:

shell
git submodule update --init

就可以看到代码被拉下来了:

但只有一层,如果想递归的 init 和 update,可以这样:

shell
git submodule update --init --recursive

这样它就会把每一层 submodule 都拉下来。

这样就完整下载了整个项目的代码。

当然,这一步可以提前到 git clone,也就是执行:

shell
git clone --recursive-submodules https://xxx.xx

这样就不用单独 git submodule init 和 git submodule update 了。

小结

小结下 git submodule 的用法:

  • 通过 git submodule add 在一个项目目录下添加另一个 git 项目作为 submodule
  • submodule 下可以单独 pull、push、add、commit 等
  • 父项目只是记录了 gitmodules 的 url 和它最新的 commit,并不管具体内容是什么
  • submodule 可以多层嵌套
  • git clone 的时候可以 --recursive-submodules 来递归初始化 submodules,或者单独执行 git submodule init 和 git submodule update

可以体会到啥叫复用子项目代码的同时保留项目的独立性了么?

subtree

然后我们再来试试 git subtree。

还是这样一个项目:

我们用 subtree 的命令添加子项目:

shell
git subtree add --prefix=child https://gitee.com/luxcurl/git-research-child.git master

这样和 submodule 有什么区别呢?

不知道你有没有发现,child 目录下是没有 .git 的,这代码它不是一个单独的 git 项目,只是一个普通目录:

所以你在这个目录下的任何改动都可以被检测到:

可以和整个项目一起 git add、commit、push 等。

不过 subtree 的方式在创建目录的时候会生成一个 commit:

那这样都作为一个普通目录了,这个子项目还独立么?还能单独 pull 和 push 么?

可以的!

虽然没有单独的 .git 目录,但它依然有独立性。你可以通过 subtree 的命令来 pull 和 push 它的代码。

比如我们先试试 pull。

我在 git-search-child 这个项目下加两个 commit:

加了 555、666 这俩 commit。

然后我在项目下执行 git subtree pull:

shell
git subtree pull --prefix=child https://gitee.com/luxcurl/git-research-child.git master

这样子项目的最新改动就拉下来了:

所以说 subtree 虽然把它作为普通目录来管理了,但它依然保留着独立 pull 和 push 到单独项目的能力!

上面的 url 如果你觉得敲起来麻烦,可以放到 git remote 里来管理:

shell
git remote add child https://gitee.com/luxcurl/git-research-child.git

这样就可以只写它的名字了:

shell
git subtree pull --prefix=child child master

这样 pull,会生成 3 个 commit,刚拉下来的 555、666 的 commit,还有一个 merge commit。

你也可以加个 --squash 来合并:

shell
git subtree pull --prefix=child child master --squash

这样就只有一个合并后的 commit,一个 merge commit 了。这就是 --squash 的作用。

再来试下独立的 push。

shell
git subtree push --prefix=child child master

这样就把它 push 上去了。

注意,这里可不是整个项目的 push,而是把那个子项目目录的改动 push 到了子项目里去。

另一个项目里就可以把它拉下来。

那问题来了,不是都没有 .git 目录了么?

那 subtree 是怎么知道哪些 commit 是新的,是属于这个子项目的呢?

还记得 subtree add 的时候单独生成了一个 commit 么?

git 会遍历 git log,直到找到这个 commit,然后把之间的 commit 里涉及到那个目录的改动摘出来,单独 push 到子项目。

最后,git submodule 在 clone 的时候需要单独拉一下子项目代码,那 git subtree 呢?

我们试试:

shell
git clone https://gitee.com/luxcurl/git-research.git git-research-3

可以看到,拉下来的就是全部的代码。

也就是说它真的就是个普通目录,只不过可以单独的作为子项目 pull 和 push 而已。

这就 git subtree 的使用方式。

小结

  • git subtree add 可以在一个目录下添加另一个子项目
  • 子项目目录和别的目录没有区别,目录下改动会被 git 检测到
  • 可以用 git subtree pull 和 git subtree push 单独提交和拉取子项目代码
  • git subtree pull 加一个 --squash 可以合并拉下来的 commit

Released under the MIT License.