git submodule vs git subtree

Git 仓库 A 包含 Git 仓库 B,若在 B 中做改动,一些诡异的情况会发生。若 B 中的改动没有提交,A 看起来就像什么都没发生一样。而当改动提交之后,A 中就会显示类似 Subproject commit 3fd38c0c070ed6b44c2e9b9551343bc75522ce25 的信息(git diff 显示的结果)。
而提交 A 到远程仓库后再拉取下来,仓库 B 竟是一个空的目录。必须再拉取 B 的代码才能得到一个完整的仓库。
针对这些问题有两种解决方法,分别是 git submodule 和 git subtree。

git submodule

1
2
// 新增 submodule
git submodule add https://github.com/KayWu/ExViewPager child

会将 git 仓库中的代码拷贝到 child 目录中,同时添加 .gitmodules 配置文件。

1
2
3
4
// .gitmodules
[submodule "child"]
path = child
url = https://github.com/KayWu/ExViewPager

child 目录被当做一个独立的 Git 仓库,所有的 Git 命令都可以在 child 目录以及上层项目下独立工作。
尽管 child 是子目录,当你不在 child 目录时并不记录它的内容。取而代之的是,当你在那个子目录里修改并提交时,子项目会通知那里的 HEAD 已经发生变更并记录你当前正在工作的那个提交。而此时上层项目会显示 child 目录下的改动,将它记录成来自那个仓库的一个特殊的提交,如同上文举例的 Subproject commit 3fd38c0c070ed6b44c2e9b9551343bc75522ce25
若他人要克隆该项目,会发现 child 目录为空。这时需要执行 git submodule init 来初始化你的本地配置文件,以及 git submodule update 拉取数据并切换到合适的提交。而后每次从主项目拉取子模块的变更时,由于主项目只更新了子模块提交的引用而没有更新子模块目录下的代码,必须执行 git submodule update 来更新子模块代码。

git subtree

1
2
3
// 添加 subtree
// git subtree add --prefix=目录 项目地址 分支
git subtree add --prefix=child https://github.com/KayWu/ExViewPager master

不同于 git submodule,此时的 child 仅仅是含有相关代码的普通目录,而不是一个独立的 Git 仓库。因此当在 child 进行修改时,上层项目会立刻记录其改动,而不是像之前那样先在子项目中提交才能进行记录。克隆上层仓库时 child 目录也不再为空。但同时,child 也不能再执行独立的 Git 命令,只有 git subtree 相关的操作。

1
2
3
// 提交改动
// git subtree push --prefix=目录 项目地址 分支
git subtree push --prefix=child https://github.com/KayWu/ExViewPager master

提交改动时 Git 会遍历所有的 commit,从中找出 child 相关的修改然后提交到 child 的项目地址上。

1
2
3
// 更新改动
// git subtree pull --prefix=目录 项目地址 分支
git subtree pull --prefix=child https://github.com/KayWu/ExViewPager master

更新 child 目录下的代码。
简便起见,可将项目地址添加为 remote 方便后续操作。

1
2
3
4
// 添加 remote
git add remote child https://github.com/KayWu/ExViewPager
// 用 remote 替换项目地址
git subtree push --prefix=child child master

简单来说,submodule 和 subtree 最大的区别是,submodule 保存的是子仓库的 link,而 subtree 保存的是子仓库的 copy。

参考