Git
章节 ▾ 第二版

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 不同,请使用其他人可以访问的 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 <[email protected]>
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 会在您运行 git submodule update --remote 时尝试更新您**所有**子模块。如果您有很多子模块,您可能希望传递要尝试更新的子模块的名称。

从项目远程获取上游更改

现在让我们站在您的合作者的角度,他们拥有 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 对所有支持它的命令(除了 clone)使用 --recurse-submodules 标志。

在拉取超级项目更新时可能会发生一种特殊情况:上游存储库可能在您拉取的某个提交中更改了 .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 发现两个分支在子模块的历史记录中记录了不同的点,需要进行合并。它将此解释为“合并未找到的后续提交”,这很令人困惑,但稍后我们将解释原因。

要解决这个问题,您需要确定子模块应该处于什么状态。奇怪的是,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 后,子模块将显示为“已修改”,并指示“新提交”。这是因为在默认情况下,子模块状态在切换分支时不会被传递。

这可能非常令人困惑,因此在项目包含子模块时,始终使用 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