-
A1. 附录 A: Git 在其他环境
- A1.1 图形界面
- A1.2 Visual Studio 中的 Git
- A1.3 Visual Studio Code 中的 Git
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine 中的 Git
- A1.5 Sublime Text 中的 Git
- A1.6 Bash 中的 Git
- A1.7 Zsh 中的 Git
- A1.8 PowerShell 中的 Git
- A1.9 小结
-
A2. 附录 B: 在应用程序中嵌入 Git
-
A3. 附录 C: 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
文件)一起受版本控制。它会与你的项目其余部分一起被推送和拉取。这就是其他克隆此项目的人知道从何处获取子模块项目的方式。
注意
|
由于 |
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
请注意,为了安全起见,你应该运行 git submodule update
时带上 --init
标志,以防你刚拉取的主项目提交添加了新的子模块;如果任何子模块有嵌套子模块,则带上 --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-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
-
首先我们解决冲突。
-
然后我们返回主项目目录。
-
我们可以再次检查 SHA-1。
-
解决冲突的子模块条目。
-
提交我们的合并。
这可能有点令人困惑,但实际上并不难。
有趣的是,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
子模块命令,可以在每个子模块中运行任意命令。如果你在同一个项目中有多个子模块,这会非常有用。
例如,假设我们想开始一个新功能或进行一次 bug 修复,并且我们正在几个子模块中进行工作。我们可以轻松地将所有子模块中的所有工作暂存起来。
$ 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 --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
将子模块置于正确状态。
幸运的是,你可以通过设置配置选项 submodule.recurse
来告诉 Git(>=2.14)始终使用 --recurse-submodules
标志: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 不同,销毁子模块目录不会丢失你拥有的任何提交或分支。
有了这些工具,子模块可以成为一种相当简单有效的方法,用于同时开发多个相关但仍独立的的项目。