章节 ▾ 第二版

7.11 Git 工具 - 子模块

子模块

在开发一个项目时,你经常会需要使用另一个项目。例如,你可能正在使用一个第三方库,或者你正在独立开发并用于多个父项目的库。在这些场景中,一个常见的问题是:你希望能够将这两个项目视为独立的,但仍然能够在一个项目中使用另一个项目。

下面是一个例子。假设你正在开发一个网站并创建 Atom feed。你决定使用一个库来生成 Atom,而不是自己编写代码。你很可能会从共享库(如 CPAN 安装或 Ruby gem)中引入此代码,或者将源代码复制到你自己的项目树中。包含库的问题在于很难以任何方式自定义库,并且通常更难部署它,因为你需要确保每个客户端都拥有该库。将代码复制到你自己的项目中的问题是,当上游更改可用时,你所做的任何自定义更改都很难合并。

Git 使用子模块解决了这个问题。子模块允许你将一个 Git 仓库作为另一个 Git 仓库的子目录。这使得你可以将另一个仓库克隆到你的项目中,并保持你的提交相互独立。

开始使用子模块

我们将逐步开发一个简单的项目,该项目已拆分为一个主项目和几个子项目。

让我们首先将一个现有的 Git 仓库添加为我们正在处理的仓库的子模块。要添加新的子模块,请使用 `git submodule add` 命令以及你想要开始跟踪的项目的绝对或相对 URL。在此示例中,我们将添加一个名为“DbConnector”的库。

$ git submodule add https://github.com/chaconinc/DbConnector
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.

默认情况下,子模块会将子项目添加到与仓库同名的目录中,在本例中为“DbConnector”。如果你想将其放到其他位置,可以在命令末尾添加不同的路径。

如果你此时运行 `git status`,你会注意到一些事情。

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	new file:   .gitmodules
	new file:   DbConnector

首先,你应该注意到新的 `.gitmodules` 文件。这是一个配置文件,存储项目 URL 和你拉取它的本地子目录之间的映射。

[submodule "DbConnector"]
	path = DbConnector
	url = https://github.com/chaconinc/DbConnector

如果你有多个子模块,此文件中将有多个条目。重要的是要注意,此文件与你的其他文件(如 `.gitignore` 文件)一起进行版本控制。它会随着项目的其余部分一起推送和拉取。这就是其他人克隆此项目时知道从哪里获取子模块项目的方式。

注意

由于 `.gitmodules` 文件中的 URL 是其他人首先尝试克隆/获取的地址,因此请确保使用他们可以访问的 URL(如果可能)。例如,如果你推送的 URL 与其他人拉取的 URL 不同,请使用其他人可以访问的 URL。你可以使用 `git config submodule.DbConnector.url PRIVATE_URL` 在本地覆盖此值以供自己使用。在适用时,相对 URL 会很有帮助。

`git status` 输出中的另一个列表是项目文件夹条目。如果你对其运行 `git diff`,你会看到一些有趣的东西

$ git diff --cached DbConnector
diff --git a/DbConnector b/DbConnector
new file mode 160000
index 0000000..c3f01dc
--- /dev/null
+++ b/DbConnector
@@ -0,0 +1 @@
+Subproject commit c3f01dc8862123d317dd46284b05b6892c7b29bc

尽管 `DbConnector` 是你工作目录中的子目录,但 Git 将其视为子模块,并且当你不在该目录中时,它不会跟踪其内容。相反,Git 将其视为该仓库中的特定提交。

如果你想要更美观的 diff 输出,可以将 `--submodule` 选项传递给 `git diff`。

$ git diff --cached --submodule
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..71fc376
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "DbConnector"]
+       path = DbConnector
+       url = https://github.com/chaconinc/DbConnector
Submodule DbConnector 0000000...c3f01dc (new submodule)

当你提交时,你会看到类似这样的内容

$ git commit -am 'Add DbConnector module'
[master fb9093c] Add DbConnector module
 2 files changed, 4 insertions(+)
 create mode 100644 .gitmodules
 create mode 160000 DbConnector

请注意 `DbConnector` 条目的 `160000` 模式。这是 Git 中的一种特殊模式,基本上表示你正在将提交记录为目录条目,而不是子目录或文件。

最后,推送这些更改

$ git push origin master

克隆带有子模块的项目

在这里,我们将克隆一个包含子模块的项目。当你克隆这样的项目时,默认情况下你会得到包含子模块的目录,但其中没有任何文件。

$ git clone https://github.com/chaconinc/MainProject
Cloning into 'MainProject'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 14 (delta 1), reused 13 (delta 0)
Unpacking objects: 100% (14/14), done.
Checking connectivity... done.
$ cd MainProject
$ ls -la
total 16
drwxr-xr-x   9 schacon  staff  306 Sep 17 15:21 .
drwxr-xr-x   7 schacon  staff  238 Sep 17 15:21 ..
drwxr-xr-x  13 schacon  staff  442 Sep 17 15:21 .git
-rw-r--r--   1 schacon  staff   92 Sep 17 15:21 .gitmodules
drwxr-xr-x   2 schacon  staff   68 Sep 17 15:21 DbConnector
-rw-r--r--   1 schacon  staff  756 Sep 17 15:21 Makefile
drwxr-xr-x   3 schacon  staff  102 Sep 17 15:21 includes
drwxr-xr-x   4 schacon  staff  136 Sep 17 15:21 scripts
drwxr-xr-x   4 schacon  staff  136 Sep 17 15:21 src
$ cd DbConnector/
$ ls
$

`DbConnector` 目录存在,但为空。你必须从主项目运行两个命令:`git submodule init` 以初始化本地配置文件,以及 `git submodule update` 以从该项目获取所有数据并检出超项目中所列的适当提交。

$ git submodule init
Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector'
$ git submodule update
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.
Submodule path 'DbConnector': checked out 'c3f01dc8862123d317dd46284b05b6892c7b29bc'

现在你的 `DbConnector` 子目录处于你之前提交时的确切状态。

还有另一种更简单的方法。如果你将 `--recurse-submodules` 传递给 `git clone` 命令,它将自动初始化并更新仓库中的每个子模块,包括嵌套子模块(如果仓库中的任何子模块本身包含子模块)。

$ git clone --recurse-submodules https://github.com/chaconinc/MainProject
Cloning into 'MainProject'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 14 (delta 1), reused 13 (delta 0)
Unpacking objects: 100% (14/14), done.
Checking connectivity... done.
Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector'
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.
Submodule path 'DbConnector': checked out 'c3f01dc8862123d317dd46284b05b6892c7b29bc'

如果你已经克隆了项目但忘记了 `--recurse-submodules`,可以通过运行 `git submodule update --init` 来组合 `git submodule init` 和 `git submodule update` 步骤。要同时初始化、获取和检出任何嵌套子模块,你可以使用万无一失的 `git submodule update --init --recursive`。

使用子模块处理项目

现在我们有一个包含子模块的项目副本,我们将与我们的团队成员一起处理主项目和子模块项目。

从子模块远程拉取上游更改

在项目中,使用子模块最简单的模型是,你只是使用一个子项目,并希望不时从它获取更新,但实际上并不修改你检出中的任何内容。让我们通过一个简单的例子来说明。

如果你想检查子模块中的新工作,可以进入该目录并运行 `git fetch` 和 `git merge` 上游分支来更新本地代码。

$ git fetch
From https://github.com/chaconinc/DbConnector
   c3f01dc..d0354fc  master     -> origin/master
$ git merge origin/master
Updating c3f01dc..d0354fc
Fast-forward
 scripts/connect.sh | 1 +
 src/db.c           | 1 +
 2 files changed, 2 insertions(+)

现在,如果你回到主项目并运行 `git diff --submodule`,你可以看到子模块已更新,并获得添加到其中的提交列表。如果你不想每次运行 `git diff` 时都输入 `--submodule`,可以通过将 `diff.submodule` 配置值设置为“log”来将其设置为默认格式。

$ git config --global diff.submodule log
$ git diff
Submodule DbConnector c3f01dc..d0354fc:
  > more efficient db routine
  > better connection routine

如果你此时提交,那么当其他人更新时,你将把子模块锁定为包含新代码。

如果你不想手动在子目录中获取和合并,也有一个更简单的方法。如果你运行 `git submodule update --remote`,Git 将进入你的子模块并为你获取和更新。

$ git submodule update --remote DbConnector
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   3f19983..d0354fc  master     -> origin/master
Submodule path 'DbConnector': checked out 'd0354fc054692d3906c85c3af05ddce39a1c0644'

此命令默认情况下将假定你希望将检出更新到远程子模块仓库的默认分支(远程上 `HEAD` 指向的分支)。但是,如果你愿意,可以将其设置为不同的值。例如,如果你希望 `DbConnector` 子模块跟踪该仓库的“stable”分支,你可以在 `.gitmodules` 文件中设置它(这样其他人也跟踪它),或者只在你的本地 `.git/config` 文件中设置它。让我们在 `.gitmodules` 文件中设置它

$ git config -f .gitmodules submodule.DbConnector.branch stable

$ git submodule update --remote
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   27cf5d3..c87d55d  stable -> origin/stable
Submodule path 'DbConnector': checked out 'c87d55d4c6d4b05ee34fbc8cb6f7bf4585ae6687'

如果你省略了 `-f .gitmodules`,它只会为你更改,但将该信息与仓库一起跟踪可能更有意义,这样其他人也这样做。

当我们此时运行 `git status` 时,Git 将显示子模块有“新提交”。

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

  modified:   .gitmodules
  modified:   DbConnector (new commits)

no changes added to commit (use "git add" and/or "git commit -a")

如果你设置配置设置 `status.submodulesummary`,Git 还会向你显示子模块更改的简短摘要

$ git config status.submodulesummary 1

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   .gitmodules
	modified:   DbConnector (new commits)

Submodules changed but not updated:

* DbConnector c3f01dc...c87d55d (4):
  > catch non-null terminated lines

此时,如果你运行 `git diff`,我们可以看到我们既修改了 `.gitmodules` 文件,也拉取了许多提交,并准备将其提交到我们的子模块项目。

$ git diff
diff --git a/.gitmodules b/.gitmodules
index 6fc0b3d..fd1cc29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,4 @@
 [submodule "DbConnector"]
        path = DbConnector
        url = https://github.com/chaconinc/DbConnector
+       branch = stable
 Submodule DbConnector c3f01dc..c87d55d:
  > catch non-null terminated lines
  > more robust error handling
  > more efficient db routine
  > better connection routine

这非常酷,因为我们实际上可以看到我们即将提交到子模块中的提交日志。提交后,当你运行 `git log -p` 时,你也可以事后看到这些信息。

$ git log -p --submodule
commit 0a24cfc121a8a3c118e0105ae4ae4c00281cf7ae
Author: Scott Chacon <schacon@gmail.com>
Date:   Wed Sep 17 16:37:02 2014 +0200

    updating DbConnector for bug fixes

diff --git a/.gitmodules b/.gitmodules
index 6fc0b3d..fd1cc29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,4 @@
 [submodule "DbConnector"]
        path = DbConnector
        url = https://github.com/chaconinc/DbConnector
+       branch = stable
Submodule DbConnector c3f01dc..c87d55d:
  > catch non-null terminated lines
  > more robust error handling
  > more efficient db routine
  > better connection routine

当你运行 `git submodule update --remote` 时,Git 默认会尝试更新你*所有*子模块。如果你有很多子模块,你可能只想传递你想要更新的子模块的名称。

从项目远程拉取上游更改

现在让我们站在协作者的角度,他们有自己的 MainProject 仓库的本地克隆。简单地执行 `git pull` 来获取你新提交的更改是不够的

$ git pull
From https://github.com/chaconinc/MainProject
   fb9093c..0a24cfc  master     -> origin/master
Fetching submodule DbConnector
From https://github.com/chaconinc/DbConnector
   c3f01dc..c87d55d  stable     -> origin/stable
Updating fb9093c..0a24cfc
Fast-forward
 .gitmodules         | 2 +-
 DbConnector         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

$ git status
 On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   DbConnector (new commits)

Submodules changed but not updated:

* DbConnector c87d55d...c3f01dc (4):
  < catch non-null terminated lines
  < more robust error handling
  < more efficient db routine
  < better connection routine

no changes added to commit (use "git add" and/or "git commit -a")

默认情况下,`git pull` 命令会递归地获取子模块更改,如上面第一个命令的输出所示。但是,它不会**更新**子模块。`git status` 命令的输出显示了这一点,它显示子模块已“修改”并有“新提交”。更重要的是,显示新提交的括号指向左侧(<),表明这些提交已记录在 MainProject 中,但不存在于本地 `DbConnector` 检出中。要完成更新,你需要运行 `git submodule update`

$ git submodule update --init --recursive
Submodule path 'vendor/plugins/demo': checked out '48679c6302815f6c76f1fe30625d795d9e55fc56'

$ git status
 On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working tree clean

请注意,为了安全起见,你应该使用 `--init` 标志运行 `git submodule update`,以防你刚刚拉取的主项目提交添加了新的子模块,并且如果任何子模块包含嵌套子模块,则应使用 `--recursive` 标志。

如果你想自动化此过程,可以将 `--recurse-submodules` 标志添加到 `git pull` 命令(从 Git 2.14 开始)。这将使 Git 在拉取后立即运行 `git submodule update`,使子模块处于正确状态。此外,如果你想让 Git 始终使用 `--recurse-submodules` 进行拉取,可以将配置选项 `submodule.recurse` 设置为 `true`(这适用于 Git 2.15 及更高版本的 `git pull`)。此选项将使 Git 对所有支持它的命令(除了 `clone`)使用 `--recurse-submodules` 标志。

在拉取超项目更新时可能发生一种特殊情况:上游仓库可能在您拉取的其中一个提交中更改了`.gitmodules`文件中的子模块URL。例如,如果子模块项目更改了其托管平台,则可能发生这种情况。在这种情况下,如果超项目引用了一个在您的本地仓库中配置的子模块远程中找不到的子模块提交,则`git pull --recurse-submodules`或`git submodule update`可能会失败。为了解决这种情况,需要`git submodule sync`命令

# copy the new URL to your local config
$ git submodule sync --recursive
# update the submodule from the new URL
$ git submodule update --init --recursive

处理子模块

如果你正在使用子模块,很有可能是因为你真的希望在处理主项目代码的同时处理子模块中的代码(或跨多个子模块)。否则,你可能会使用更简单的依赖管理系统(如 Maven 或 Rubygems)。

所以现在让我们通过一个例子,同时对子模块和主项目进行更改,并同时提交和发布这些更改。

到目前为止,当我们运行 `git submodule update` 命令从子模块仓库获取更改时,Git 会获取更改并更新子目录中的文件,但会将子仓库留在所谓的“分离 HEAD”状态。这意味着没有本地工作分支(例如 `master`)跟踪更改。由于没有工作分支跟踪更改,这意味着即使你将更改提交到子模块,这些更改也很有可能在下次运行 `git submodule update` 时丢失。如果你希望跟踪子模块中的更改,则必须执行一些额外的步骤。

为了让子模块更容易进入和修改,你需要做两件事。你需要进入每个子模块并检出要处理的分支。然后你需要告诉 Git,如果你已经进行了更改,并且稍后 `git submodule update --remote` 从上游拉取了新工作,该怎么办。选项是你可以将它们合并到你的本地工作中,或者你可以尝试将你的本地工作变基到新更改之上。

首先,让我们进入子模块目录并检出一个分支。

$ cd DbConnector/
$ git checkout stable
Switched to branch 'stable'

让我们尝试使用“merge”选项更新子模块。要手动指定它,我们只需在 `update` 调用中添加 `--merge` 选项。在这里,我们将看到此子模块的服务器上有一个更改,并且它已被合并。

$ cd ..
$ git submodule update --remote --merge
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   c87d55d..92c7337  stable     -> origin/stable
Updating c87d55d..92c7337
Fast-forward
 src/main.c | 1 +
 1 file changed, 1 insertion(+)
Submodule path 'DbConnector': merged in '92c7337b30ef9e0893e758dac2459d07362ab5ea'

如果我们进入 `DbConnector` 目录,我们已经将新更改合并到我们的本地 `stable` 分支中。现在让我们看看当我们对库进行自己的本地更改时会发生什么,同时其他人也向上游推送了另一个更改。

$ cd DbConnector/
$ vim src/db.c
$ git commit -am 'Unicode support'
[stable f906e16] Unicode support
 1 file changed, 1 insertion(+)

现在,如果我们更新子模块,我们可以看到当我们进行了本地更改并且上游也有需要合并的更改时会发生什么。

$ cd ..
$ git submodule update --remote --rebase
First, rewinding head to replay your work on top of it...
Applying: Unicode support
Submodule path 'DbConnector': rebased into '5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94'

如果你忘记了 `--rebase` 或 `--merge`,Git 只会将子模块更新为服务器上的任何内容,并将你的项目重置为分离 HEAD 状态。

$ git submodule update --remote
Submodule path 'DbConnector': checked out '5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94'

如果发生这种情况,不用担心,你可以简单地回到目录中并再次检出你的分支(它仍然包含你的工作),然后手动合并或变基 `origin/stable`(或你想要的任何远程分支)。

如果你没有在子模块中提交你的更改,并且你运行了一个可能导致问题的 `submodule update`,Git 将会获取更改,但不会覆盖子模块目录中未保存的工作。

$ git submodule update --remote
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 0), reused 4 (delta 0)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   5d60ef9..c75e92a  stable     -> origin/stable
error: Your local changes to the following files would be overwritten by checkout:
	scripts/setup.sh
Please, commit your changes or stash them before you can switch branches.
Aborting
Unable to checkout 'c75e92a2b3855c9e5b66f915308390d9db204aca' in submodule path 'DbConnector'

如果你所做的更改与上游的某些更改冲突,当你运行更新时,Git 会通知你。

$ git submodule update --remote --merge
Auto-merging scripts/setup.sh
CONFLICT (content): Merge conflict in scripts/setup.sh
Recorded preimage for 'scripts/setup.sh'
Automatic merge failed; fix conflicts and then commit the result.
Unable to merge 'c75e92a2b3855c9e5b66f915308390d9db204aca' in submodule path 'DbConnector'

你可以进入子模块目录并像往常一样解决冲突。

发布子模块更改

现在我们的子模块目录中有一些更改。其中一些是通过我们的更新从上游引入的,另一些是本地进行的,并且由于我们尚未推送它们,因此其他人尚无法使用。

$ git diff
Submodule DbConnector c87d55d..82d2ad3:
  > Merge from origin/stable
  > Update setup script
  > Unicode support
  > Remove unnecessary method
  > Add new option for conn pooling

如果我们在主项目中提交并推送它,而没有同时推送子模块更改,那么尝试检出我们更改的其他人将会遇到麻烦,因为他们将无法获取所依赖的子模块更改。这些更改只会存在于我们的本地副本中。

为了确保这种情况不会发生,你可以要求 Git 在推送主项目之前检查所有子模块是否已正确推送。`git push` 命令接受 `--recurse-submodules` 参数,该参数可以设置为“check”或“on-demand”。“check”选项将使 `push` 在任何已提交的子模块更改尚未推送时简单地失败。

$ git push --recurse-submodules=check
The following submodule paths contain changes that can
not be found on any remote:
  DbConnector

Please try

	git push --recurse-submodules=on-demand

or cd to the path and use

	git push

to push them to a remote.

如你所见,它还为我们提供了下一步可能需要做的一些有用的建议。简单的选项是进入每个子模块并手动推送到远程,以确保它们是外部可用的,然后再次尝试此推送。如果你希望“check”行为在所有推送中发生,可以通过执行 `git config push.recurseSubmodules check` 使此行为成为默认。

另一个选项是使用“on-demand”值,它会尝试为你完成此操作。

$ git push --recurse-submodules=on-demand
Pushing submodule 'DbConnector'
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (8/8), done.
Writing objects: 100% (9/9), 917 bytes | 0 bytes/s, done.
Total 9 (delta 3), reused 0 (delta 0)
To https://github.com/chaconinc/DbConnector
   c75e92a..82d2ad3  stable -> stable
Counting objects: 2, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 266 bytes | 0 bytes/s, done.
Total 2 (delta 1), reused 0 (delta 0)
To https://github.com/chaconinc/MainProject
   3d6d338..9a377d1  master -> master

正如你所看到的,Git 进入 `DbConnector` 模块并将其推送,然后才推送主项目。如果该子模块推送由于某种原因失败,则主项目推送也将失败。你可以通过执行 `git config push.recurseSubmodules on-demand` 使此行为成为默认。

合并子模块更改

如果你在更改子模块引用的同时有其他人也在更改它,你可能会遇到一些问题。也就是说,如果子模块历史记录已分歧并提交到超项目中的分歧分支,则你可能需要做一些工作来解决。

如果其中一个提交是另一个的直接祖先(快速合并),那么 Git 将简单地选择后者进行合并,这样就可以了。

然而,Git 不会为你尝试进行简单的合并。如果子模块提交分歧并且需要合并,你将得到类似这样的内容

$ git pull
remote: Counting objects: 2, done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 2 (delta 1), reused 2 (delta 1)
Unpacking objects: 100% (2/2), done.
From https://github.com/chaconinc/MainProject
   9a377d1..eb974f8  master     -> origin/master
Fetching submodule DbConnector
warning: Failed to merge submodule DbConnector (merge following commits not found)
Auto-merging DbConnector
CONFLICT (submodule): Merge conflict in DbConnector
Automatic merge failed; fix conflicts and then commit the result.

所以基本上这里发生的是,Git 已经发现这两个分支记录的子模块历史中的点是分歧的,需要合并。它将其解释为“未找到以下提交的合并”,这令人困惑,但我们稍后会解释原因。

为了解决这个问题,你需要弄清楚子模块应该处于什么状态。奇怪的是,Git 在这里没有提供太多信息来帮助你,甚至没有历史两侧提交的 SHA-1。幸运的是,这很容易弄清楚。如果你运行 `git diff`,你可以获取你尝试合并的两个分支中记录的提交的 SHA-1。

$ git diff
diff --cc DbConnector
index eb41d76,c771610..0000000
--- a/DbConnector
+++ b/DbConnector

所以,在这种情况下,`eb41d76` 是我们子模块中**我们**拥有的提交,`c771610` 是上游拥有的提交。如果我们进入子模块目录,它应该已经在 `eb41d76` 上,因为合并不会触及它。如果由于某种原因它不在那里,你可以简单地创建并检出一个指向它的分支。

重要的是来自另一侧提交的 SHA-1。这就是你必须合并和解决的内容。你可以直接尝试用 SHA-1 合并,或者你可以为此创建一个分支,然后尝试合并。我们建议后者,即使只是为了创建一个更好的合并提交消息。

所以,我们将进入子模块目录,基于 `git diff` 中的第二个 SHA-1 创建一个名为“try-merge”的分支,然后手动合并。

$ cd DbConnector

$ git rev-parse HEAD
eb41d764bccf88be77aced643c13a7fa86714135

$ git branch try-merge c771610

$ git merge try-merge
Auto-merging src/main.c
CONFLICT (content): Merge conflict in src/main.c
Recorded preimage for 'src/main.c'
Automatic merge failed; fix conflicts and then commit the result.

我们在这里遇到了实际的合并冲突,所以如果我们解决它并提交它,那么我们就可以简单地用结果更新主项目。

$ vim src/main.c (1)
$ git add src/main.c
$ git commit -am 'merged our changes'
Recorded resolution for 'src/main.c'.
[master 9fd905e] merged our changes

$ cd .. (2)
$ git diff (3)
diff --cc DbConnector
index eb41d76,c771610..0000000
--- a/DbConnector
+++ b/DbConnector
@@@ -1,1 -1,1 +1,1 @@@
- Subproject commit eb41d764bccf88be77aced643c13a7fa86714135
 -Subproject commit c77161012afbbe1f58b5053316ead08f4b7e6d1d
++Subproject commit 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a
$ git add DbConnector (4)

$ git commit -m "Merge Tom's Changes" (5)
[master 10d2c60] Merge Tom's Changes
  1. 首先我们解决冲突。

  2. 然后我们回到主项目目录。

  3. 我们可以再次检查 SHA-1。

  4. 解决冲突的子模块条目。

  5. 提交我们的合并。

这可能有点令人困惑,但实际上并不难。

有趣的是,Git 处理了另一种情况。如果子模块目录中存在一个合并提交,其中包含历史记录中的**两个**提交,Git 会将其作为可能的解决方案建议给你。它发现子模块项目中的某个时刻,有人合并了包含这两个提交的分支,所以你可能想要那个。

这就是为什么之前的错误消息是“未找到以下提交的合并”,因为它无法完成**此操作**。这令人困惑,因为谁会期望它会**尝试**这样做呢?

如果它确实找到了一个可接受的合并提交,你会看到类似这样的内容

$ git merge origin/master
warning: Failed to merge submodule DbConnector (not fast-forward)
Found a possible merge resolution for the submodule:
 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a: > merged our changes
If this is correct simply add it to the index for example
by using:

  git update-index --cacheinfo 160000 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a "DbConnector"

which will accept this suggestion.
Auto-merging DbConnector
CONFLICT (submodule): Merge conflict in DbConnector
Automatic merge failed; fix conflicts and then commit the result.

Git 提供的建议命令将更新索引,就像你运行了 `git add` (这会清除冲突) 然后提交一样。但你可能不应该这样做。你可以同样轻松地进入子模块目录,查看差异,快速前进到此提交,正确测试它,然后提交它。

$ cd DbConnector/
$ git merge 9fd905e
Updating eb41d76..9fd905e
Fast-forward

$ cd ..
$ git add DbConnector
$ git commit -am 'Fast forward to a common submodule child'

这实现了同样的目的,但至少这样你可以验证它是否有效,并且当你完成时,你的子模块目录中会有代码。

子模块提示

你可以做一些事情来使使用子模块更容易一些。

子模块 foreach

有一个 `foreach` 子模块命令可以在每个子模块中运行一些任意命令。如果你在同一个项目中有多个子模块,这会非常有用。

例如,假设我们要开始一个新功能或进行错误修复,并且我们正在几个子模块中进行工作。我们可以轻松地将所有子模块中的所有工作暂存起来。

$ git submodule foreach 'git stash'
Entering 'CryptoLibrary'
No local changes to save
Entering 'DbConnector'
Saved working directory and index state WIP on stable: 82d2ad3 Merge from origin/stable
HEAD is now at 82d2ad3 Merge from origin/stable

然后我们可以在所有子模块中创建新分支并切换到它。

$ git submodule foreach 'git checkout -b featureA'
Entering 'CryptoLibrary'
Switched to a new branch 'featureA'
Entering 'DbConnector'
Switched to a new branch 'featureA'

你懂的。一个非常有用的事情是,你可以生成一个统一的差异,显示你的主项目和所有子项目中的更改。

$ git diff; git submodule foreach 'git diff'
Submodule DbConnector contains modified content
diff --git a/src/main.c b/src/main.c
index 210f1ae..1f0acdc 100644
--- a/src/main.c
+++ b/src/main.c
@@ -245,6 +245,8 @@ static int handle_alias(int *argcp, const char ***argv)

      commit_pager_choice();

+     url = url_decode(url_orig);
+
      /* build alias_argv */
      alias_argv = xmalloc(sizeof(*alias_argv) * (argc + 1));
      alias_argv[0] = alias_string + 1;
Entering 'DbConnector'
diff --git a/src/db.c b/src/db.c
index 1aaefb6..5297645 100644
--- a/src/db.c
+++ b/src/db.c
@@ -93,6 +93,11 @@ char *url_decode_mem(const char *url, int len)
        return url_decode_internal(&url, len, NULL, &out, 0);
 }

+char *url_decode(const char *url)
+{
+       return url_decode_mem(url, strlen(url));
+}
+
 char *url_decode_parameter_name(const char **query)
 {
        struct strbuf out = STRBUF_INIT;

在这里我们可以看到我们在子模块中定义了一个函数并在主项目中调用它。这显然是一个简化的例子,但希望它能让你了解这可能有多大用处。

有用的别名

你可能需要为其中一些命令设置别名,因为它们可能很长,而且你无法为其中大多数命令设置配置选项以使其成为默认值。我们在Git 别名中介绍了如何设置 Git 别名,但如果你计划经常使用 Git 子模块,这里有一个你可能想要设置的示例。

$ git config alias.sdiff '!'"git diff && git submodule foreach 'git diff'"
$ git config alias.spush 'push --recurse-submodules=on-demand'
$ git config alias.supdate 'submodule update --remote --merge'

这样,当你想要更新子模块时,只需运行 `git supdate`,或者运行 `git spush` 以推送并进行子模块依赖检查。

子模块问题

然而,使用子模块并非没有问题。

切换分支

例如,在 Git 2.13 版本之前的 Git 中,切换包含子模块的分支也可能很棘手。如果你创建一个新分支,在那里添加一个子模块,然后切换回没有该子模块的分支,你仍然会看到子模块目录作为一个未跟踪的目录

$ git --version
git version 2.12.2

$ git checkout -b add-crypto
Switched to a new branch 'add-crypto'

$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
...

$ git commit -am 'Add crypto library'
[add-crypto 4445836] Add crypto library
 2 files changed, 4 insertions(+)
 create mode 160000 CryptoLibrary

$ git checkout master
warning: unable to rmdir CryptoLibrary: Directory not empty
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	CryptoLibrary/

nothing added to commit but untracked files present (use "git add" to track)

删除目录并不困难,但把它放在那里可能会有点令人困惑。如果你删除它,然后切换回包含该子模块的分支,你需要运行 `submodule update --init` 来重新填充它。

$ git clean -ffdx
Removing CryptoLibrary/

$ git checkout add-crypto
Switched to branch 'add-crypto'

$ ls CryptoLibrary/

$ git submodule update --init
Submodule path 'CryptoLibrary': checked out 'b8dda6aa182ea4464f3f3264b11e0268545172af'

$ ls CryptoLibrary/
Makefile	includes	scripts		src

同样,这真的不难,但可能会有点令人困惑。

较新版本的 Git (Git >= 2.13) 通过向 `git checkout` 命令添加 `--recurse-submodules` 标志来简化这一切,该标志负责将子模块置于我们正在切换到的分支的正确状态。

$ git --version
git version 2.13.3

$ git checkout -b add-crypto
Switched to a new branch 'add-crypto'

$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
...

$ git commit -am 'Add crypto library'
[add-crypto 4445836] Add crypto library
 2 files changed, 4 insertions(+)
 create mode 160000 CryptoLibrary

$ git checkout --recurse-submodules master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

nothing to commit, working tree clean

当你在超项目中处理多个分支时,每个分支的子模块都指向不同的提交时,使用 `git checkout` 的 `--recurse-submodules` 标志也会很有用。事实上,如果你在记录子模块不同提交的分支之间切换,执行 `git status` 后,子模块将显示为“modified”,并指示“new commits”。这是因为在切换分支时,子模块状态默认不会继承。

这可能非常令人困惑,因此当你的项目有子模块时,最好始终 `git checkout --recurse-submodules`。对于没有 `--recurse-submodules` 标志的旧版 Git,在检出后你可以使用 `git submodule update --init --recursive` 将子模块置于正确状态。

幸运的是,你可以告诉 Git (>=2.14) 始终使用 `--recurse-submodules` 标志,通过设置配置选项 `submodule.recurse`:`git config submodule.recurse true`。如上所述,这也会使 Git 在每个支持 `--recurse-submodules` 选项的命令中(除了 `git clone`)递归到子模块中。

从子目录切换到子模块

许多人遇到的另一个主要问题涉及从子目录切换到子模块。如果你一直在项目中跟踪文件,并且希望将它们移到子模块中,则必须小心,否则 Git 会对你发火。假设你的项目子目录中有文件,并且你希望将其切换到子模块。如果你删除子目录,然后运行 `submodule add`,Git 会对你大喊

$ rm -Rf CryptoLibrary/
$ git submodule add https://github.com/chaconinc/CryptoLibrary
'CryptoLibrary' already exists in the index

你必须先取消暂存 `CryptoLibrary` 目录。然后才能添加子模块

$ git rm -r CryptoLibrary
$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.

现在假设你在一个分支中完成了上述操作。如果你尝试切换回那些文件仍然存在于实际树中而不是子模块中的分支——你会收到此错误

$ git checkout master
error: The following untracked working tree files would be overwritten by checkout:
  CryptoLibrary/Makefile
  CryptoLibrary/includes/crypto.h
  ...
Please move or remove them before you can switch branches.
Aborting

你可以使用 `checkout -f` 强制切换,但请注意,如果其中有未保存的更改,它们可能会被该命令覆盖。

$ git checkout -f master
warning: unable to rmdir CryptoLibrary: Directory not empty
Switched to branch 'master'

然后,当你切换回来时,你会得到一个空的 `CryptoLibrary` 目录,原因不明,`git submodule update` 也可能无法修复它。你可能需要进入子模块目录并运行 `git checkout .` 才能找回所有文件。你可以在 `submodule foreach` 脚本中运行此命令,以便为多个子模块运行。

重要的是要注意,如今子模块将其所有 Git 数据都保存在顶级项目的 `.git` 目录中,因此与旧版 Git 不同,销毁子模块目录不会丢失你拥有的任何提交或分支。

有了这些工具,子模块可以成为同时开发几个相关但仍然独立的项目的相当简单有效的方法。