章节 ▾ 第二版

7.11 Git 工具 - 子模块

子模块

在处理一个项目时,经常需要用到该项目中的另一个项目。 也许是第三方开发的库,或者您正在单独开发并在多个父项目中使用。 在这些情况下,一个常见的问题是:您希望能够将这两个项目视为单独的项目,但仍然能够从一个项目中使用另一个项目。

举个例子。 假设您正在开发一个网站并创建 Atom feeds。 您决定使用一个库,而不是编写自己的 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。 您可以使用 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 将其视为该仓库中的特定提交。

如果您想要更好的差异输出,您可以将 --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 initgit submodule update 步骤。 为了也初始化、获取和检出任何嵌套的子模块,您可以使用万无一失的 git submodule update --init --recursive

处理带有子模块的项目

现在我们拥有一个包含子模块的项目副本,并将与我们的队友在主项目和子模块项目上进行协作。

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

在项目中使用子模块的最简单模型是,如果您只是使用一个子项目,并且希望不时地从中获取更新,但实际上并没有修改结帐中的任何内容。 让我们在那里举一个简单的例子。

如果您想检查子模块中的新工作,您可以进入该目录并运行 git fetchgit 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,以防您刚刚拉取的 MainProject 提交添加了新的子模块,并且如果任何子模块具有嵌套的子模块,则使用 --recursive 标志。

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

当拉取超级项目更新时,可能会发生一种特殊情况:上游仓库可能在您拉取的提交之一中更改了 .gitmodules 文件中子模块的 URL。 例如,如果子模块项目更改其托管平台,则可能会发生这种情况。 在这种情况下,如果超级项目引用了在您的仓库本地配置的子模块远程中找不到的子模块提交,则 git pull --recurse-submodulesgit 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”选项更新我们的子模块。 要手动指定它,我们可以简单地将 --merge 选项添加到我们的 update 调用中。 在这里,我们将看到服务器上对此子模块进行了更改,并将其合并到其中。

$ 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已经发现这两个分支记录了子模块历史记录中发散并且需要合并的点。它将其解释为“merge following commits not found”,这令人困惑,但我们将在稍后解释原因。

要解决此问题,你需要弄清楚子模块应该处于什么状态。奇怪的是,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进行合并,也可以为其创建一个分支,然后尝试将其合并。我们建议后者,即使只是为了创建一个更好的合并提交消息。

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

$ 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会建议你将其作为可能的解决方案。它看到在子模块项目的某个时刻,有人合并了包含这两个提交的分支,所以也许你想要那个。

这就是之前的错误消息是“merge following commits not found”的原因,因为它无法完成此操作。这令人困惑,因为谁会期望它尝试这样做呢?

如果它确实找到了单个可接受的合并提交,你将看到类似以下内容

$ 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'

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

子模块提示

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

Submodule 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.recursegit 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 不同,销毁子模块目录不会丢失任何你拥有的提交或分支。

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

scroll-to-top