Git子模块在我不知道的情况下提交..?

1 git github git-submodules pull-request

一些个人背景:我以前使用过 git 子模块,但说实话,我不是粉丝,完全接受这可能是我对它们工作方式的细微差别的无知。当我之前将它们用于共享库时,我git pull在子模块中使用 a 提取更新,然后modified … (new commits)在父模块中显示。我得到这个-我已经改变了提交的指针.git沿着这些线路的元数据,或什么的,我需要commitpush父回购这种变化,所以它用正确的子模块相关的承诺。

我的问题:我现在有一份新工作,我从事的项目有一个子模块。它不是共享库 - 构建过程依赖于一些凭据,出于安全原因,这些凭据每天更新并通过子模块分发。所以每天都有一个拉取更新的过程,它发生在git submodule update --remote.

两个奇怪的地方:(1)子模块永远处于分离头状态;(2)status父模块保持干净,子模块没有变化。

那么为什么这是一个问题呢?问题是,我的拉取请求与“已更改文件”列表中的子模块一起显示。我不认为这实际上会导致问题,但一位同行评审员对这些变化有特别的例外,因为它们不应该存在。由于子模块从未在git status输出中显示为已修改,因此我不知道我如何提交任何更改,以及如何阻止它。

(这是 GitHub 中的私有存储库 - 我有一个分支,并且正在我的分支的一个分支中工作。子模块仍然指向未分支的主存储库。)

(显然这在其他一些开发人员的 PR 中也很明显,但不是在每个 PR 中。)

我是这家公司的新手,我可以不用被描绘成无法正常进行版本控制的人!但是我需要一个比我更具有 git 智慧的人来告诉我发生了什么。

(就我个人而言,我的解决方案是不使用 submodule,这可能不在我的控制范围内。)

tor*_*rek 5

TL;DR:除了脾气暴躁的评论者之外,这里可能没有任何问题。:-) 如果你git add稍微改变你的流程或者对你的公关建设大惊小怪,你可以让他更开心,但是让他对更新不那么暴躁可能会更好,或者实际上根本不使用子模块(但是这两个更像是一个团队讨论项目)。如果您熟悉下面的大部分内容,您可以跳到子模块更新过程的部分。

子模块从根本上来说有点笨拙,并且总会有一些问题围绕着它们。

首先,让我们解决这些问题:

子模块永远处于分离头状态

这是正常的。一个分离的 HEAD 只是意味着我不在任何分支上,我有一个特定的提交检出,并且子模块不在这样的任何分支上是正常的。

status母公司保持清洁,没有显示出改变的子模块

这也很正常(我们稍后会看到详细信息)。两者都与您的公关问题无关(至少直接)。

子模块是它自己的 Git 存储库

现在,让我们解决这一部分:

每天都有一个拉取更新的过程,它发生在 git submodule update --remote

如果我们查阅git submodule资料,我们发现,此子是记录(不是很好)的下部分--remote的选项:

该选项仅对update命令有效。不要使用超级项目记录的 SHA-1 来更新子模块,而是使用子模块的远程跟踪分支的状态。使用的遥控器是分支的遥控器(branch.<name>.remote),默认为origin. 使用的远程分支默认为master,但分支名称可以通过设置或(优先)中的submodule.<name>.branch选项来覆盖。.gitmodules.git/config.git/config

这适用于任何受支持的更新程序......唯一的变化是目标 SHA-1 的源...... [snip]

这里有很多东西要解开。让我们从最简单的部分开始:子模块本身就是一个 Git 存储库,具有分支名称、标签名称、 a HEAD、索引、工作树等。因此有两个 Git 存储库:一个用于超级项目(将子模块列为要使用的存储库),另一个用于子模块本身。子模块没有列出任何特别的东西。 唯一的特殊项目是这些,这是超级项目 Git 在进行任何克隆或git checkout-ing之前做一些摆弄的结果:

  • 包含实际子模块的.git(或$GIT_DIR) 目录往往位于超级项目的.git目录中;1
  • 子模块的工作树位于超级项目确定的路径上;和
  • 超级项目 Git 检出和/或认为是正确的提交,因为子模块——通常,无论如何——由记录在 superproject 中哈希 ID确定。

最后一项是分离的 HEAD 的来源。 将子模块提取到其工作树的步骤运行,其中最初来自超级项目提交。运行让超级项目 Git 告诉子项目 Git:git checkout hash-IDhash-IDgit submodule update --remote

  • 首先,运行git fetch,以便我们可以查看您的分支是否有新的提交哈希;
  • 这时如果有一个新的提交哈希值,运行切换到它。git checkout hash

当然,这也会导致分离的 HEAD。最奇怪的部分是描述为查看您的分支是否有新的提交哈希的步骤,因为子模块不在分支上! 它有一个分离的 HEAD。如果你在这里对自己说“WTF”,那么你就走对了(没有双关语)。下面的最后一段--remote有答案:

...update --remote使用 ... submodule.<name>.branch[找出子模块的分支名称,从而确定是否有新的提交哈希,如果有,则将其传递给git checkout.]

(你可以update --remote使用 checkout、merge 或 rebase 中的任何一个。使用后两者时,它更复杂。我们不需要更多的复杂性,所以让我们坚持这个checkout案例。)


1此功能是 Git 2.12 中的新功能,当时添加了“absorbgitdirs”。以前,.git子模块的 位于子模块工作树的根部。现在发生的事情是子模块 Git 写入了一个以它的工作树的根命名的文件.git。该.git文件指示子模块 Git 查看超级项目的.git目录,以便它可以看到它是超级项目的子模块。


超级项目提交记录子模块哈希 ID

关于 Git 存储库中的提交有一个通用规则,它适用于所有存储库和所有提交:它们都是所有内容的完整快照。对于子模块存储库来说是这样——每次提交都是所有文件的完整快照——对于超级项目也是如此。然而,超级项目提交记录子模块的哈希 ID,而不是记录子模块的文件

这背后的机制是 Git 的index。除了--bare存储库(这些没有工作树),一个 Git 存储库带有一个索引和一个工作树。索引保存了从当前提交中产生的每个文件的副本,这些文件将进入您进行的下一次提交。

存储库中保存的文件,由每次提交记录,以特殊的、压缩的(有时是高度压缩的)、仅限 Git 的格式存储。一旦提交,这些文件也是完全只读的,这意味着如果您没有更改旧文件,新提交可以重新使用旧提交中的旧文件。这就是为什么即使每次提交存储每个文件,存储库也不会快速增长的一个重要原因:新提交实际上只是重新使用旧提交的文件。

当然,只读的文件是不能修改的,Git-only 形式的文件除了 Git 什么都不能使用。因此,Git 必须将 Git 存储在提交中的这些只读、仅 Git 文件扩展为您可以使用的读/写、普通格式的文件。那些读/写的普通格式文件进入您的工作树。

大多数版本控制系统到此为止:存储库中有永久的、只读的、冻结的、压缩的文件,以及您使用的工作树中的临时的、读/写的文件。为了进行新的提交,VCS 再次压缩每个工作树文件并检查它是否已经在存储库中。如果是这样,它会重新使用旧的;如果没有,它会放入新的;无论哪种方式,新提交都是指新文件,即使那只是旧文件。但这是非常缓慢的。

相反,Git 所做的是解冻,但保留当前提交中的每个文件的压缩和仅 Git文件。那些进入索引。然后,当您更改每个文件时,Git 会强制(程序员)访问git add每个文件:这会将文件重新压缩为 Git 专用格式并将其复制到索引中,如果有则覆盖先前的索引副本,或者从如果是全新的就划伤。无论哪种方式,索引都已准备就绪,可以进行新的提交,因此git commit速度非常快:它只需要冻结所有已经准备好的文件。

这就是为什么索引可以被描述为您将进行的下一次提交,如果您git commit现在运行。(它还具有其他几个有用的功能。因此,Git 将索引全部放在您的脸上,让您一直git add陷入其中这一事实可能很烦人,但也很有用。但是这方面——索引 = 下一次提交——是关键。)

这对文件没问题,但是对于子模块呢?那么,超级项目提交中的子模块信息就是子模块应该的哈希ID git checkout。因此,Git 将其存储在提交索引中。您进行的下一次提交将包含此子模块哈希。

子模块更新过程

git submodule update --init正如我们之前看到的,初始检出(例如)只是检出特定的提交。这会将正确的提交放入子模块,并且在超级项目的索引中也有正确的提交哈希

Submodule path 'sub': checked out '8ffac73422c73898facacb7a0f92ed15a29cc7ad'
Run Code Online (Sandbox Code Playgroud)

我的子模块 Git 现在处于分离的 HEAD 状态。HEAD我的超级项目中的提交显示正确的子模块提交是8ffac73422c73898facacb7a0f92ed15a29cc7ad,并且索引说要使用该提交:2

$ git rev-parse HEAD:sub
8ffac73422c73898facacb7a0f92ed15a29cc7ad
$ git rev-parse :0:sub
8ffac73422c73898facacb7a0f92ed15a29cc7ad
Run Code Online (Sandbox Code Playgroud)

当你运行时git submodule update --remote,它会检出一些新的提交,它不会记录新的子模块哈希 ID,它只是检出它。在这里,我更新了子模块的远程存储库,以便在子模块中git submodule update --remote找到一个新的哈希 ID master(只有一个子模块分支,所以一切都是自动的master):

$ git submodule update --remote
Submodule path 'sub': checked out 'ca09e95a23e28ef71765113ea0caef2bd7ce9594'
Run Code Online (Sandbox Code Playgroud)

现在子模块在那个提交上:

$ (cd sub; git rev-parse HEAD)
ca09e95a23e28ef71765113ea0caef2bd7ce9594
Run Code Online (Sandbox Code Playgroud)

但是,我所在的超级项目仍然需要另一个提交:

$ git status
On branch 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:   sub (new commits)

no changes added to commit (use "git add" and/or "git commit -a")
Run Code Online (Sandbox Code Playgroud)

git diff命令,该指数比对工作树,他说:

$ git diff
diff --git a/sub b/sub
index 8ffac73..ca09e95 160000
--- a/sub
+++ b/sub
@@ -1 +1 @@
-Subproject commit 8ffac73422c73898facacb7a0f92ed15a29cc7ad
+Subproject commit ca09e95a23e28ef71765113ea0caef2bd7ce9594
Run Code Online (Sandbox Code Playgroud)

我现在可以运行git add subgit commit进行与旧提交几乎完全相同的新提交,除了它告诉 Git 提取的哈希 ID,如果我要运行git submodule update-没有 --remote- 现在是ca09e95a23e28ef71765113ea0caef2bd7ce9594

$ git add sub
$ git commit -m 'update submodule'
[master fd09d9b] update submodule
 1 file changed, 1 insertion(+), 1 deletion(-)
Run Code Online (Sandbox Code Playgroud)

如果我有其他更改或新文件,我也必须将git add它们复制到索引中,以便它们进入新的提交。

请注意,如果我小心地避免 git add sub——还有像git add -aor 之类的东西git add -u,它们会更新sub——那么我所做的任何提交都不会有新的哈希 ID sub,而是会有旧的哈希 ID sub。如果有人检出那个特定的提交,然后运行git submodule update(没有--remote再次),他们的超级项目 Git 会告诉他们的子模块 Git 检出旧的提交,而不是新的提交。

如果您不小心git add使用git reset了sub,也可以在提交之前将其设置回原位:

$ git status
On branch 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:   sub (new commits)

no changes added to commit (use "git add" and/or "git commit -a")
$ git add sub   # oops!
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   sub

$ git reset sub
Unstaged changes after reset:
M       sub
Run Code Online (Sandbox Code Playgroud)

现在,即使子模块本身打开ca09e95a23e28ef71765113ea0caef2bd7ce9594,超级项目中的索引仍然显示8ffac73422c73898facacb7a0f92ed15a29cc7ad


2这些名称HEAD:sub:0:sub,是用于指定特定对象的gitrevisions 语法。该git rev-parse命令将它们转换为底层 Git 对象的哈希 ID。在这种情况下,这是子模块的树或索引哈希 ID。


关于拉取请求

Git 本身没有拉取请求。3 这些是 GitHub 等网站的功能。Git 真正拥有的只是提交,存储在存储库中。要在 GitHub 上创建拉取请求:

  1. 您必须git push对某个实际存储库进行实际提交。这可以是主要的,也可以是您使用 GitHub“分叉存储库”按钮创建的辅助。Git 需要你的提交以某种方式连接到这个主存储库。当然,最主要的一个主要的一个-那是相当不错连接! -和GitHub的“叉”按钮可以使一个幕后的链接你的叉子的主要原因之一,所以无论是将服务。

  2. 现在,您的提交是在GitHub上的某个地方,无论是在或链接到主存储库,您可以使用更多的GitHub的Web界面clicky按钮选择一个特定的分支的主要存储库。然后,GitHub 在幕后尝试使用特定于 GitHub 的引用名称执行git merge4。如果合并顺利,GitHub 会向操作主存储库的任何人提交拉取请求,允许他们使用 Web 界面上的点击按钮进行合并。

因此,您通过此拉取请求获得的实际上是其他人重复git merge您调用的a的能力。这git merge将做什么对你来说很容易说:你可以git merge自己做。因此,当且仅当 agit merge也成功更改子模块哈希时,拉取请求才会更改子模块哈希。它什么也做不了。它甚至可能在子模块哈希上发生合并冲突!

所以:什么时候会git merge改变子模块哈希?这与 agit merge将更改任何其他文件时相同。什么git merge是找到合并基础提交,然后实际上运行两个 git diff命令:一个将合并基础与您要合并的分支的尖端进行比较,另一个将合并基础与您要合并的分支的尖端进行比较正在. Git 然后将两组更改应用于所有更改的文件,从合并库中的文件开始。

假设您要将develop工作的地方合并到master(via git checkout master && git merge develop) 中。请注意,--ours现在是master分支及其提交,--theirs而是您的提交:您已经切换了角色,成为稍后将要单击 GitHub“合并”按钮的那个人。那么,三个有趣的提交是:

  • 提示master:这是左侧,或本地,或--ours提交;
  • 提示develop:这是右侧,或远程,或其他,或--theirs提交;和
  • 合并基础(无论git merge-base --all master develop打印什么哈希 ID ,假设它只打印一个哈希 ID)。

如果 base-vs-master 更改子模块哈希,但 base-vs-develop 确实更改了子模块哈希,则合并将成功更改子模块哈希:合并获取他们(您的)更改。

如果 base-vs-master 确实更改了子模块哈希,但 base-vs-develop 更改子模块哈希,则合并将成功并保留master哈希:合并不会拾取他们(您的)更改,因为没有此类更改。

如果base-VS-master 不会改变子模块哈希基VS-develop 改变了哈希子模块,他们有更好的改变都散相同的散列。如果是,则更改匹配,Git 会执行一项更改。如果没有,更改会发生冲突,并且 Git 会声明合并冲突并停止(或者,GitHub 发出无法合并的拉取请求)。

所以,这里的伎俩,如果你想提供更改子模块哈希ID,为确保您的拉动请求提交-一个将是--theirs,当谈到时间提交合并,使用相同的子模块哈希值作为在合并基础中,无论提交可能是什么。请注意,masterand的合并基础develop取决于存储在masterand 中的提交哈希develop。如果master随着时间的推移而变化——通常是这样——你在星期二计算的合并基哈希可能在星期三是错误的。因此,在某种程度上——实际上,在很大程度上——追逐子模块的合并基础的提交哈希是一个毫无意义的差事。只有在发生合并冲突时才需要它,在这种情况下,更容易抓住master直接提交的哈希 ID,因此这两个更改(base-vs-master 和 base-vs-develop)是相同的更改。

最后,这意味着这些哈希 ID 冲突(如果发生)通常只是小麻烦。您可以尝试通过避免git adding 子模块或git reset-ing来避免让您的提交更新子模块哈希 ID(永远),如果您不小心添加了它。(您还必须避免git commit -a哪个会添加它,然后提交,而不给您重置它的机会。)


3 Git 有一个命令 ,git request-pull它会生成一封电子邮件,建议某人使用您控制的存储库git pullgit fetch从您控制的存储库获取提交。要使用此命令,请将提交放入存储库,让其他人可以使用您的存储库,构建电子邮件消息,并将其发送给其他人。它当时他们来运行git fetch或者git pull手动,使用存储库URL从他们的结束。

(GitHub clicky button 界面对于大多数人来说使用起来要简单得多。)

4从技术上讲,GitHub 必须在这里做一些特别的事情,因为他们所有的存储库都是--bare存储库,没有工作树。如果git merge没有工作树,该命令将无法运行。但是他们无论如何都在以一种特殊的方式进行合并并且不会解决任何冲突,因此他们只关心可以自动运行完成的那些。如果您git merge在自己的非裸存储库中执行一个操作,并且它会自动完成,那么 GitHub 的也会自动完成并执行与您相同的操作。