首先,让我们尝试澄清一些事情。当你自己使用Git时,它是很复杂的。当你添加第二个Git仓库 - 你可以将自己的提交推送到其中一个Git仓库,并从中获取其他人的新提交 - 那就更加复杂了。子模块不过是第三个Git仓库,而这个第三个Git仓库有第四个Git仓库,你或许可以将自己的提交推送到其中一个Git仓库,并从中获取其他人的新提交。因此,我们立刻面临至少四个Git仓库的情况,所有这些仓库都有点独立于彼此。
我们可以尝试画图,但即使是图片也会有点混乱。我们如何区分这四个仓库?Git为其中两个仓库命名:你直接与这两个仓库交互。其中一个是超级项目,这是你运行git diff
并看到的地方:
diff --git a/submodule-path b/submodule-path
index 5a8162f..2ff89a2 160000
--- a/submodule-path
+++ b/submodule-path
@@ -1 +1 @@
-Subproject commit 5a8162ff9a602deb96956854346988e1ee45672e
+Subproject commit 2ff89a2bfcaa014885a70b0da86e997ecd8d0688
另一个重要的部分是子模块本身:如果你
cd submodule-path
并运行各种Git命令,你会发现它是一个普通的Git仓库。唯一不寻常的是它几乎总是在“分离头指针”模式下。
你的超级项目Git可能有一个
origin
。这是一个仓库,技术上来说,是你可以在这个Git中使用的一个简短的名称,用于引用另一个仓库 - 你可以在超级项目中进行提交并将这些新提交推送到
origin
。这个超级项目提交确切地包含了什么?我们马上就会看到。
你的子模块Git同样也有一个
origin
。那是另一个不同的Git仓库:这个不太好画的图中的第四个Git仓库。我不确定你是否想将提交发送到那个第四个仓库。但是,你肯定想从那个仓库获取提交。有多种方法可以做到这一点,包括使用
git submodule update --remote
,也许还带有其他选项。我更喜欢只是
cd submodule-path
并直接开始在子模块Git中工作,因为这将问题简化为你已经知道如何操作的事情:根据出现在其
origin
中的新提交来操作一个本地Git仓库。
假设你只想在子模块中拾取一些新的提交
如果是这种情况,你可以:
cd submodule-path
git fetch
git checkout origin/master
现在子模块已经作为其分离的HEAD使用所需的提交。 从这里开始没有任何东西需要git push:在此存储库中驻留的所有提交都是从此子模块的origin Git获得的提交。
(使用git submodule update --remote可能会为您执行cd submodue-path和git fetch和git checkout origin/master,而无需更改自己的工作目录。整个操作在其自己的子shell中运行,以便这些cd操作不会影响您所在的位置。这似乎就是您的git submodule update --remote --merge所做的:没有必要创建新的合并提交,因此它只是切换到由某个分支名称标识的提交上的origin。)
但是,如果您需要在子模块中进行新提交...
在这种情况下,您可能希望强制子模块处于分支上,以便您可以在更正常的工作流程中工作。然后,您可能想要git checkout master,然后执行各种命令。最终,您可能会在子模块存储库中创建一个新提交,您需要将其git push到子模块的origin存储库,以便其他人也能够获取此提交。
您可以让子模块保持在其分支上。子模块的分支与超级项目Git无关:超级项目Git仅关心子模块中检出的提交。(这就是为什么在上面的早期情况下,我们可以轻松切换分离的HEAD。)
现在子模块Git已经处于正确的提交上,您必须创建一个新的超级项目提交。
此时,您可以从子模块回到超级项目。您将在git diff输出中看到与您上面引用的内容完全相同的内容,并且git status将显示:
modified: submodule-path (new commits)
这并不一定意味着子模块仓库中有任何未在子模块仓库“origin”中的新提交。它只是意味着子模块仓库处于(作为其HEAD,无论是否分离)不是当前超级项目状态所应在的提交上。
问题在于当前超级项目提交在某种程度上是有缺陷的。它曾经是正确的,但现在不再是了,就像如果您在本地编辑了文件“README.md”,当前超级项目提交也将有缺陷一样。这意味着您需要在超级项目中进行一个新的、已更正的提交。超级项目 Git 将根据超级项目仓库索引中的内容创建新提交,因此现在您需要更新索引。
如果您更改了文件“README.md”,那么更新索引的方法如下:
git add README.md
但你改变的不是一个README.md
文件,而是子模块哈希ID。因此,你需要在索引中记录新的哈希ID。方法如下:
git add submodule-path
这将通过运行
cd submodule-path; git rev-parse HEAD
从子模块中获取哈希ID - 显示在
git diff
中的原始哈希ID,并将该哈希ID插入索引中。现在
git diff
- 比较索引和工作树 - 将不再显示这些
Subproject commit
行,但是
git diff --cached
- 比较当前(超级项目)提交和索引 - 会显示它们。现在
git status
将说这些“新提交”已准备好提交,而不是尚未暂存以提交。
您可以在此时
git add
任何其他超级项目文件(如果有需要在索引中更新的文件)。然后:
git commit
在超级项目中,将创建一个新的提交记录,该记录将记录您在子模块路径上运行
git add
时放入超级项目索引中的哈希ID。
您(本地)已完成更新,但这里有一些需要考虑的事情
请注意,超级项目中的每个提交都记录了子模块的哈希ID。每次使用git checkout
切换到不同的超级项目提交时,它不仅会将正确的超级项目文件提取到(超级项目)索引和您的(超级项目)工作树中,还会将记录的子模块哈希ID提取到(超级项目)索引中。默认情况下,它不会cd
进入子模块并按其哈希ID检出该特定提交。您可以通过设置或添加--recursive
到git checkout
来更改此设置;或者您可以只运行git submodule update
,这会告诉您的Git依次cd
进入每个子模块,并git checkout
当前在(超级项目)索引中记录的哈希ID。
在某个时间点,您需要git push
在超级项目中创建的新提交到origin
(超级项目的来源),以便新提交及其新记录的哈希ID出现在超级项目的origin
Git中。您可以随时执行此操作,但是假设您在子模块中创建了新提交,并且尚未在子模块本身中使用git push
将这些新提交发送到子模块的origin
。在这种情况下,在超级项目中创建的新提交记录了一个提交的哈希ID,该提交仅存在于您本地的子模块存储库中。如果有人运行git fetch
到超级项目Git的origin
,他们将从您发送的新提交中获取此新哈希ID,但无法在他们的子模块克隆中找到该提交。因此,如果确实进行了新的子模块提交,则通常最好先git push
它们,然后再git push
新的超级项目提交。
(如果您没有进行任何新的子模块提交,则没有问题。)
git submodule update -f --init
,但它会重置到第一个提交,而我需要最后一个。 - Ivan Kushupdate
子命令只是将子模块强制更新到超级项目中记录的提交。但在 任何 Git 仓库中,并不一定存在单个“最新”的提交。我会写点东西,但请记住,子模块本质上是复杂的。 - torek