TL;DR
您的问题是使用--remote。别这样了。
长
你在VonC's answer 上提到了in a comment:
当我[运行]git status[我得到]
modified: app/Services/Notification (new commits)
modified: app/Services/Payment (new commits)
modified: database/migrations (new commits)
(new commits) 部分表示:您的子模块积极使用(通过其当前结帐)的提交哈希 ID 与您的 index 的提交哈希 ID 不同(建议接下来commit) 说应该使用。
这里有很多行话(“submodules”、“gitlinks”、“index”、“commit hash ID”),因此需要解压很多。我们稍后会讨论这个问题。
请注意,上面git status 的输出是您在原始问题中引用的git diff 输出的更紧凑的表示:
diff --git a/app/Services/Payment b/app/Services/Payment
index 72602bc..a726378 160000
--- a/app/Services/Payment
+++ b/app/Services/Payment
@@ -1 +1 @@
-Subproject commit 72602bc5d9e7cef136043791242dfdcfd979370c
+Subproject commit a7263787e5515abe18e7cfe76af0f26d9f62ceb4
我们在这里看到的是,对于app/Services/Payment,您的(主、顶级、“或超级项目”存储库的索引表明此特定子模块应该使用提交72602bc5d9e7cef136043791242dfdcfd979370c。但它是实际上改用了 commit a7263787e5515abe18e7cfe76af0f26d9f62ceb4。我们刚刚添加了一个术语来定义:superproject。
一些初始定义
让我们从 Git 存储库的定义开始。存储库的核心是一对数据库。一个是 commits 和其他内部 Git 对象的数据库。另一个数据库包含名称——人类可读的名称,因为 Git 为其自己的对象使用的名称是难以理解的。
commit 是 Git 存储在第一个(通常更大)数据库中的四种内部对象之一。这些提交被编号,编号非常大,最高可达 2160-1。这些数字以hexadecimal 表示,例如72602bc5d9e7cef136043791242dfdcfd979370c。 (提交是您通常以我们将要描述的方式与之交互的唯一提交,因此我们将方便地忽略其余三个,但它们也都有编号。)
这些数字看起来是随机的,尽管它们实际上是加密散列函数的输出,因此完全是非随机的。它们来自散列函数这一事实也是我们称它们为 hash IDs 的原因。但这里真正的重点是,它们似乎完全被打乱了,没有一个人类会记住它们。为此,我们需要一台计算机。
幸运的是,我们拥有一台电脑。我们只是让计算机为我们记住这些哈希 ID,使用诸如分支名称和标签名称之类的东西。每个提交还在其自身内部存储哈希 ID 或一些先前的提交。我们真的不需要担心这一点,但这就是分支在 Git 中的真正工作方式。
所以:
- 一个存储库是
- 一对数据库,其中一个数据库保存提交
- 具有哈希 ID 或大而丑陋的数字。
我们和 Git 使用名称的第二个数据库来查找特定提交的哈希 ID,我们使用提交来查找更多提交的哈希 ID,等等。
提交是只读的:工作树和索引
现在,了解关于这些提交(以及所有Git 的内部对象)的关键在于它们都是只读。它们必须是,因为散列技巧:散列 ID 是 进入内部对象的每一位的函数,我们通过散列 找到对象ID,因此哈希 ID 必须始终匹配。如果我们从数据库中提取的某个对象的哈希 ID 与我们用来在数据库中找到它的哈希 ID 不匹配,Git 会判定数据库已损坏。1
所以提交是完全只读的。不仅如此,每个提交中的文件——我们之前没有定义,但每个提交都包含每个文件的完整快照——采用特殊的 Git-only 格式,压缩和解压缩重复,只有 Git 可以读取。 (字面上没有什么可以覆盖它们,因为一切都是只读的。)
这意味着仅仅为了使用某个提交,我们必须提取那个提交。 Git 将通过以下方式提取提交:
- 读取提交中的压缩文件和 Git 化文件;
- 将它们扩展为普通的读/写文件;和
- 将这些文件写入工作树。
这个工作树——另一种行话——是我们实际工作的地方。在这里,我们可以查看、读取甚至写入文件。它们以 文件的形式存在,而不是作为只读的、仅限 Git 的数据库条目存在。所以,现在我们可以完成工作了。
工作树还使我们能够进行 new 提交,但在这里,Git 插入了一个额外的绊脚石。在 Git允许我们进行新的提交之前,Git 要求我们将所有更新的文件复制回 Git。
这一步实际上有一定的意义,因为我们在工作树中看到和处理/使用的文件根本不是 在 Git 中。它们可能是从 Git 中复制出来的(从提交或其支持对象之一中复制出来的),但是一旦它们被复制出来,它们就会被复制出来。
Git 调用 Git 让我们用三个不同的名称重新复制更新文件的地方:index,作为一个名称本身没有意义; 暂存区,它指的是我们和 Git 如何使用索引——以及 缓存,它几乎不再使用但仍然显示例如,git rm --cached 中的标志。
索引的角色作为暂存区非常简单。它在合并冲突期间扮演了一个扩展的角色,但由于我们在这里不担心这些,我们将看看我们和 Git 如何将它用作暂存区域。
当我们第一次签出提交时——使用git checkout 或git switch——Git 需要将所有压缩和 Git 化的文件展开到我们的工作树中。但是 Git 偷偷地将这些文件中的每一个的“副本”粘贴到它的索引/暂存区域中。我在这里将“复制”一词放在引号中,因为 Git 的内部文件副本都是去重。这就是为什么即使每次提交都存储每个文件,Git 存储库也不会变得非常庞大的原因:大多数提交重用大多数文件,在这种情况下,重用文件根本不占用空间,因为它已被重复数据删除。
这些索引“副本”也是如此:它们是重复的,因为有问题的文件在提交中。所以索引“副本”不占用空间。2 但是进行新 commit 的关键是:索引副本正是要进入 下一次提交。
换句话说,索引保存您的建议的下一次提交。现在,在对某些现有提交进行了“干净”检查后,索引与提交匹配。但是现在您可以根据需要修改工作树中的一些文件。修改工作树文件后,需要将其复制回 Git 的索引。您可以使用git add 执行此操作,其中:
- 读取工作树副本;
- 压缩它,否则 Git 化它;
- 检查结果是否重复;和
- 如果它是副本,则使用原始文件(丢弃 Git 化的临时副本),否则使用新的 Git 化文件,并使用它来更新索引。
结果是索引现在包含您提议的下一次提交——就像它在 之前您运行 git add 一样。只是现在,您提议的下一次提交已更新。
您对要更新的所有文件重复此操作:在工作树中更新它们,然后,迟早,但总是在运行git commit 之前,根据需要运行git add。 add 步骤会根据您添加的内容更新您的提议的下一次提交。 (请注意,一个全新的文件也会进入索引,以同样的方式,它只是不必踢出一些现有的去重副本。)
因此我们现在知道两件事:
-
工作树保存着有用的文件副本。
-
暂存区——或索引——保存提议的下一次提交,您在更新工作树后更新。
当您运行 git commit 时,Git 会简单地打包 当时索引中的所有内容,并将其作为 Git 化、只读、存储的集合放入新提交中- 永久压缩和删除重复文件。3
1目前我们能做的非常有限。处理损坏的最常见方法是完全丢弃数据库并从一个好的副本中克隆一个新的,因为 Git 是分布式的,并且每个存储库都有数千个副本“在那里”,所以效果很好。当然,如果没有其他副本,它就会停止工作。
2它们需要一些空间来保存文件名、内部 blob 哈希 ID 和一堆缓存数据——这就是名称 cache 的来源再次——通常每个文件不到 100 字节:现在几乎没有。
3如果你使用git commit -a,注意这大致相当于运行:
git add -u
git commit
也就是说,-a 选项真正所做的只是在提交之前插入“更新”样式git add。 Git 仍然从 (updated-by-add) 索引构建新的提交。不过,这里有几个技术复杂性。这些与原子性和 Git 钩子的操作有关。将它们放在一起意味着如果您使用预提交钩子,您必须非常聪明地编写这些预提交钩子,和/或避免使用git commit -a .不过,这里不是详细介绍的地方。
子模块导致 Git 存储库爆炸式增长
现在你知道了:
我们正准备继续使用 Git 的 子模块。
Git 子模块的最短定义是它是另一个 Git 存储库。不过,这个定义可能有点太短了。它遗漏了一个关键项,所以让我们再试一次:submodule 是:
- 一个 Git 存储库,其中
- 其他一些 Git 存储库指这个 Git 存储库;和
- 其他一些 Git 存储库对这个 Git 存储库进行一些控制。
我们现在知道必须至少涉及两个 Git 存储库,并且一个存储库被置于某种监管位置。
这就是我们如何定义术语超级项目:超级项目是具有子模块的Git存储库。超级项目是监督者/监督者。
一个超级项目可以是多个子模块的超级项目。 (这就是你的情况:你至少有三个子模块。所以你至少有四个 Git 存储库。)
充当主管(扮演超级项目角色)的 Git 存储库本身可以是另一个 Git 存储库的子模块。在这种情况下,“中间”存储库是子模块 和 超级项目。我不知道你是否有这些:在你的问题中没有任何证据。
现在,关于大多数 Git 存储库的一件事是:它们是其他 Git 存储库的克隆。我们主要使用克隆。因此,假设您拥有某个存储库 R0 的克隆 R1 作为您的超级项目。如果您的克隆 R1 是三个子模块的超级项目,那么这三个 Git 存储库本身可能是三个 more 存储库的克隆。所以我们突然在你的基本问题中谈论至少八个 Git 存储库!
如果有八个或更多存储库,事情很快就会变得相当混乱。不再有 存储库、 工作树、 索引等等。相反,有 8 个存储库,4 个克隆在您的计算机上,4 个 工作树,4 个 Git 索引事物,等等。
我们需要能够独立地讨论每个存储库、索引和工作树,即使它们可能有些相互依赖。这意味着我们需要为每个存储库、索引和工作树命名一。为了简化一些事情,我将为您的超级项目git clone 使用名称R,为代表app/Services/Payment 的存储库之一使用S0,以及 S1 用于其中的另一个。
这一切是如何运作的
您从某个地方(从某个存储库 R0)克隆了您的超级项目存储库 R,但在那之后,我们可以暂时停止考虑它,所以我们将想想 R 本身。您的存储库 R 有提交,这些提交包含文件等。
您选择了一些提交 in R 来签出:
git checkout somebranch
名称 somebranch 解析为原始提交哈希 ID H,这是您的 Git 从 R 中提取出来的提交,用于填充索引和工作树,以便您可以使用 R。
到目前为止,没有额外的存储库。然而,有一个名为.gitmodules 的文件来自R 中的提交H。此外,commit H 列出了一些 gitlinks。 gitlink 是一个特殊的条目,它将进入 一个提交,它包含两件事:
- 一个路径名,在本例中为
app/Services/Payment,以及
- 一些提交哈希 ID
S(在本例中为 72602bc5d9e7cef136043791242dfdcfd979370c)。
这些 gitlink 进入 R 中的 index。我们将只讨论这个特殊的 gitlink。
如果你现在运行git submodule update --init(注意这里缺少--remote),你的Git命令,在存储库R上运行,会在索引中注意到这个gitlink。 (没有对应的文件,只有 gitlink。)
您的超级项目 Git 命令,执行此 git submodule update,现在将注意到您尚未克隆任何子模块,并且由于 --init 选项,将为您运行 git clone 命令。这个git clone 命令需要一个 URL。该 URL 来自 .gitmodules 文件。
此时 Git 克隆的 存储库 是存储库 S0(可能在 GitHub 上:无论如何在某些服务器上)。克隆被隐藏起来,4 创建一个新的存储库 S1。您的 Git 软件现在在 S1 内运行git checkout 操作,以便将提交复制到工作树和索引中。
S1 的 index 隐藏在 S1 的存储库中,但 工作树 的 S1 strong>S1 被放置在app/Services/Payment:您想要从子模块中查看和使用的文件 的位置。所以现在普通目录(或文件夹,如果你更喜欢这个词)app/Services/Payment 充满了普通文件。这些构成S1的工作树。
您的子模块 S1 现在可以使用了。我们需要考虑三个存储库:R、S0 和 S1。我们有两个暂存区/index-es:一个与 R 一起使用,一个与 S1 一起使用。我们有两种工作树可供使用,一种与 R 一起使用,一种与 S1 一起使用。 S1 的工作树在 R 的工作树中,但 R repository em> 不会使用它。只有 S1 存储库会使用它。
4在现代 Git 中,克隆的 .git 目录被填充到 .git/modules/ 中的 R 中。在古老版本的 Git 中,子模块克隆进入子模块路径中的 .git——在本例中为 app/Services/Payment/.git。
git submodule update --remote
git submodule update 的 --remote 标志告诉它,而不是服从超级项目 gitlink——记住,这是 R 索引中的一个条目,在名称下app/Services/Payment,当前持有哈希 ID 72602bc5d9e7cef136043791242dfdcfd979370c——你的 Git 软件应该进入子模块 S1 并运行:
git fetch origin
这涉及到存储库 S0。存储库 S0 有 它自己的 分支和标签名称,以及 它自己的 提交。存储库 S1 之前是从 S0 克隆的,但 S0 可能随时更新。因此,git fetch 步骤与处理 S0 的 Git 软件联系,并从该 Git 获取 S0 的任何新提交并将它们放入您的克隆 S1。然后,作为最后一步,S1 中的git fetch origin 创建或更新 S1 中与 S0 中的 em>分支 名称。
这会根据 分支名称 更新您的 S1 中的(本地)origin/master、origin/develop、origin/feature/tall 等等,如 S0。您现在在 S1 中拥有来自 S0 的所有 提交*,并且您知道 它们 是哪个提交(S0) 例如,在他们的master 上调用“最新”提交。
您的git submodule update --remote 现在所做的是将您的名字origin/master 转换为哈希ID。您的 S1 Git 从该操作中获得的哈希 ID 不是 72602bc5d9e7cef136043791242dfdcfd979370c。其实是a7263787e5515abe18e7cfe76af0f26d9f62ceb4。
您的 superproject Git 现在会指示您的 S1 Git 运行:
git checkout --detach a7263787e5515abe18e7cfe76af0f26d9f62ceb4
(或与git switch 相同;无论如何,这一切都是在最新版本的Git 内部完成的,尽管旧版本在此处运行git checkout)。
这会从提交 a7263787e5515abe18e7cfe76af0f26d9f62ceb4 填充您的 S1 索引和工作树。这就是您 S1 中的当前提交。
同时,您的 superproject 存储库 R 仍然要求提交 72602bc5d9e7cef136043791242dfdcfd979370c。这就是您将在 R 中进行的新提交的索引/暂存区域中的内容。
如何处理这一切
如果您想要 R 开始调用a7263787e5515abe18e7cfe76af0f26d9f62ceb4,您只需运行:
git add app/Services/Payment
在 R 工作时。这指示 R Git 在 S1 Git 内运行 git rev-parse HEAD,它会找到当前签出提交的哈希 ID。然后此哈希 ID 进入 R 索引/暂存区,以便您在 in R中进行 next 提交> 将通过该哈希 ID 调用该提交。
如果您希望 S 将提交 72602bc5d9e7cef136043791242dfdcfd979370c 签出,您有多种选择:
(cd app/Services/Payment && git checkout --detach 72602bc5d9e7cef136043791242dfdcfd979370c)
例如会这样做。或者你可以运行git submodule update。此命令在 R 中运行,告诉 R Git 从 R 索引中读取提交哈希 ID,并在每个索引中运行 git checkout 命令子模块,强制子模块签出到所需的提交。
当您运行 git submodule update --init 时,如果您添加 --remote,您将指示您的 R Git 获取每个子模块并从某些地方找到 latest 提交源存储库中的分支(此处的示例中为 S0)。选择的分支在 R 中的不同位置定义,尽管如今它往往是 master 或 main。没有--init 的git submodule update 也是如此。 --init 仅表示如果需要,进行初始克隆。 --remote 部分表示进行提取并从远程跟踪名称获取哈希 ID。关键部分始终是 哈希 ID。来自:
控制提交你的 Git 指示子模块 Git 签出。
git status和git diff命令,运行inR,只报告索引(R的索引)和工作树(本例中为S1 的工作树结帐)匹配。如果不是,git diff 会告诉您有什么区别,而git status 只会说“它们不同”。