git 子模块和获取

git submodule and fetch

无法理解子模块

它们似乎不必要地复杂。通常我完全避免它们,但是一个项目让我陷入了这种情况

所以...

我在我们的开发服务器上有一个 git 带有子模块的回购协议

/myproject
          /.git
          /files ...
          /other
               /submodule
                         /.git

现在因为我们 运行 一个 dev/prod 环境,我们可以做的事情非常有限

我怎么

我们传统上使用先获取再合并策略,而不是单次拉取。由于团队规模很小,我们也不使用裸仓库。

我已经尝试了多种不同的方法来实现上述目标,none 似乎是正确的。似乎涉及的步骤非常多,所以我一定是做错了什么。

此外,我不希望从子模块远程仓库中获取到产品服务器的获取。

只是想让你知道我正在处理的项目是一个 drupal 8 项目,在生产环境中进行开发是完全不合适的,我们甚至没有安装 composer 或 drush。

[Submodules] appear to be unnecessarily complicated ...

可能是真的。然而,子模块也必然复杂。 :-) 我还会注意到 Git 2.x 中的子模块支持明显优于 Git 1.5 或 1.6 左右的坏日子,那时我才知道为什么人们称它们为 sob-模块。一些历史可能是这里有些复杂的原因。

在我深入探讨更长的答案之前,这里是开始的简短方法:在克隆后立即使用 git clone --recurse-submodules 或 运行 git submodule update --init --recursive。 (第二个 --recursive 仅当子模块有自己的子模块时才需要。)将 --recurse-submodules 选项添加到 git clone 只是告诉 git clone 这样做 git submodule update --init --recursive在其正常的操作顺序之后。请注意,虽然 不会 帮助您完成 子模块中工作的过程。

How do I ...

Git 是一种工具,而不是解决方案(construction business 中的一句俗语,显然,但普遍适用于大多数技术)。与大多数工具一样,有多种使用方法。

关于子模块需要了解的是每个子模块只是另一个 Git 存储库。 使 一个 Git 存储库成为“子模块”的唯一一件事是,有一些“后来的外部”存储库以某种方式控制着内部存储库。在内部存储库中,我们将外部存储库称为 superproject.

在您将在其中进行任何工作的任何 Git 存储库中,您都有一个 work-tree。 work-tree 以普通的日常形式保存文件,您(以及计算机上的其他程序)可以在其中使用它们。每个 Git 存储库还有一个 index,这是您构建下一次提交的地方。索引也被称为暂存区,有时也被称为缓存,要么反映了它极其重要的作用,要么反映了“索引”这个词的糟糕选择其原始名称(或两者)。而且,当然,每个 Git 存储库都有一个 commits 的集合,具有各种 b运行ch names and/or 标签名称 通过某种 human-readable 名称识别特定的提交哈希。

如果 Git 存储库独立存在,那么那些名称(b运行ch 和标签名称)将对我们人类有用,在该存储库中工作。但是我们刚刚声明 this 存储库是一个 submodule,它在某些 other[ 的命令下生存(或死亡) =482=] 存储库——超级项目。我们自己的 b运行ch 和标签名称几乎没用。如果我们将 this 存储库视为常规存储库,而不仅仅是某些超级项目的附属物,它们就会变得有用。当我们将此存储库视为受控实体时,我们希望此存储库有一个 分离的 HEAD。超级项目,而不是从属子模块,指示要检查的提交哈希,不是通过某种 human-readable 名称,而是通过原始哈希 ID。

这提供了所有“我如何”的答案。 superproject 在超级项目的索引中通过其原始哈希 ID 记录了 应该 在子模块中签出的特定提交。

克隆

[How do I] Clone the repo ... such that [the clone] is fully populated with ... all the submodules checked out?

像任何克隆一样,这可以通过 git 克隆 <em>url [dir]</em> 来实现,这真正包含大约六个步骤:

  1. 创建一个新的空目录 dir 并切换 (cd) 到它,或者使用一些现有的空目录,如果这样说的话:([ -d <em>dir</em> ] || mkdir <em>dir</em>) && cd <em>dir</em>. (如果失败,请停止,不要执行任何剩余步骤。如果后续步骤失败,请删除我们创建的新目录,并删除我们创建的所有文件,不留下部分失败克隆的痕迹。)如果我们不给 git clone 目录名,它会根据 url 参数计算一个。
  2. 创建一个新的空存储库:git init。这将创建 .git 目录和初始配置。
  3. 根据 git clone 后给出的 -c 选项进行任何所需的额外配置。
  4. 添加一个远程给定一个url:git远程添加<em>远程url</em>。遥控器的常用名称是 origin,但您可以使用 -o 选项控制它。
  5. 从远程获取提交:git fetch <em>remote</em>.
  6. 检查一些 b运行ch 或标签名称:git 检查 <em>name</em>。如果这是一个 b运行ch 名称,则 b运行ch 尚不存在,因此此 创建 b运行ch 的方式与git checkout 确实如此。如果这是一个标签名称,这会将提交作为分离的 HEAD 检查出来。这里的name是te 你给了一个 -b 选项。如果你没有给一个,名称是通过询问 git fetch 操作的另一端的 Git 获得的,其中 b运行ch it推荐,这很常见 master。如果这也失败了——如果另一个 Git 没有推荐的名字——使用的名字是 master.

最后一步,即第 6 步,检查一些特定的提交,通常是通过“打开”一个 b运行ch,例如 mastercreating b运行ch 名称基于在步骤 5 中获得的名称(git fetch,这使得 origin/master)。检查此特定提交的行为会填充存储库的索引和 work-tree,因此现在您的 work-tree 中拥有所有需要的文件。

子模块和gitlinks

如果您刚刚签出的提交有子模块,它有一个名为 .gitmodules 的文件,并且在您刚刚签出的提交中有一个或多个特殊条目,每个条目称为 gitlink。 gitlink 条目看起来很像文件 (blob) 条目或 tree 条目,但具有 type-code 160000 而不是 100644(常规文件)或 100755(可执行文件)或 004000(树)。1 这些 gitlink 条目进入你的索引,你的 Git 在 gitlink 给出的路径上创建一个空目录,就像你的 Git 为 treeblob.2 的文件 哈希 ID 与这些 gitlink条目——每个索引条目都有一个哈希ID——是子模块中一个特定提交的条目,Git可以,但现在还不会,作为一个分离头。

请注意,我在这里说 如果您刚刚签出的 提交 有子模块。这是另一个关键实现:子模块的“submodule-ness”由超级项目中的特定提交控制。该提交需要有一个 gitlink 条目,以提供要在子模块中签出的哈希 ID,以及一个 .gitmodules 文件。但是这个 .gitmodules 文件是做什么用的?


1符号link还有一个索引type-code、120000。它们的处理方式几乎与 blob 对象完全相同,只是只要启用了 symlinks,Git 就会将内容写入 symlink 而不是文件。如果 symlinks 被禁用,Git 将内容写入常规文件,以便您可以对其进行编辑,re-add 稍后使用 [=57] 将其作为 symlink =],如果你知道处理索引条目的所有魔法。

2Git 将为 tree 对象创建一个空目录这一事实导致人们尝试使用 Git' s semi-secret empty tree to store empty directories. Unfortunately, the index itself has weird corner cases here and Git turns the empty tree into a gitlink entry under various conditions. This then acts as a broken submodule—a gitlink without a .gitmodules entry—which makes Git behave slightly badly.


.gitmodules 文件

我们刚刚在上面看到,git clone 至少需要一个参数:要克隆的存储库的 url。超级项目将所需的 commit hash ID 存储在 gitlink 中,但它如何知道 url 使用?答案是查看 .gitmodules 文件。

.gitmodules 的内容与 .git/config$HOME/.gitconfig 或任何其他 Git 配置文件的格式相同,事实上,Git 使用 git config 阅读它们:

git config -f .gitmodules --get submodule.path/to/x.url

这寻找

[submodule "path/to/x"]
    url = <whatever you put here>

.gitmodules 文件中,当我们找到它时,那个 提供 URL.

实际上,内容将是:

[submodule "path/to/x"]
    path = path/to/x
    url = <whatever you put here>

也许还有以下一项或两项:

    branch = <name>
    update = <control>

path必须对应子模块在父工程中的相对路径,子模块名称必须是子模块在父工程中的相对路径。 (如果其中一个或另一个错误/不匹配会发生什么,我不太确定。Git 的子模块命令通常确保它们匹配,因此问题永远不会出现。)

这让 git submodule 找到 URL 来制作克隆。 这个过程比较复杂。当你运行git submodule initgit submodule update --init时,Git会复制 url 设置从 .gitmodules.git/config。如果有 update = <em>control</em> 设置,它也会复制它,除非 .git/config 中已经有设置。 (这是你提到的那些“不必要的并发症”之一,虽然我认为这是为了纠正历史错误。)

如果没有 --initgit submodule update 命令将只查看 .git/config 中的条目,而不查看 .gitmodules 中的条目。这意味着您可以使用两步序列 git submodule init && git submodule update 来做同样的事情,但 git submodule update --init 更容易输入。更重要的是,git submodule init 没有 --recursive 选项,而 git submodule update 有。这实际上是明智的,因为 git submodule init .gitmodules 复制到 .git/config(有关更多信息,请参见下文)。 git submodule update 操作实际上使用上面概述的 six-step 过程创建了克隆。

将 HEAD 分离到子模块中的正确提交上

我们看到超级项目列出了子模块的 正确 哈希 ID,作为 gitlink 条目。这意味着 Git 需要在超级项目中 start,从索引中读取 gitlink 条目,然后切换到子模块(cd <em>path</em>) 和 git checkout 通过其哈希 ID 的正确提交。这将导致一个分离的 HEAD,并签出正确的提交。

执行此操作的命令是 git submodule update。而且,这通常是我们想要的:通过其哈希 ID 作为分离的 HEAD 检查特定的提交。现在我们已经在子模块中得到了我们想要的东西,我们就完成了……或者我们?如果这个 Git 存储库怎么办——记住,每个子模块都是一个普通的 Git 存储库,就其本身而言——如果这个 Git 存储库有它自己的子模块怎么办?

子模块可以有子模块

如果这个子模块有自己的子模块,我们现在希望这个 sub-Git 到 git checkout 正确的提交,运行 git submodule init 初始化它的 .git/config对于它的子模块,运行 git submodule update 使它自己的子模块得到 checked-out 到正确的提交。这正是 git submodule update 已经代表我们的超级项目所做的,所以我们只希望这个 git submodule update 递归地操作子模块的子模块。 这意味着 git submodule update 需要能够递归到子模块以及 --init 它们。

所以 这就是 git submodule update --init --recursive 存在的原因:它是从超级项目进入每个子模块的主力,如果需要,设置它的 .git/config,检查正确的 detached-HEAD 哈希,然后在子模块的子模块上递归。

git clone 可以调用 git submodule update

如果我们现在一直倒回 git clone,我们可以看到在第 6 步之后我们需要的是第 7 步:git submodule update --init --recursive,以进入超级项目中列出的每个子模块并初始化它并检查正确的分离 HEAD,如果该子模块是其他子模块的超级项目,则递归处理它们。最后,我们将拥有超级项目及其特定提交,控制其所有子模块,这些子模块作为分离的 HEAD 在正确的提交上,并且对于那些本身就是带有子模块的超级项目的每个子模块,submodule-as-superproject的提交将递归地控制submodule-as-superproject的子模块。

如果您没有递归子模块,所有递归最终都将无所事事:这会增加一些额外的工作,但无害。所以这通常是要走的路:只需 运行 git clone --recurse-submodules 就可以创建克隆,其子模块作为分离的 HEAD 存储库检出,然后就完成了。

在子模块中工作

你有什么几乎一个单独的问题:

How do I then update a file in other/submodule?

我们在上面看到,超级项目控制/使用子模块的方式是让超级项目通过绝对哈希 ID 指定子模块将被锁定到哪个提交中,作为分离的 HEAD。这对于控制和使用子模块非常有用,除非我们想要更新子模块到一些更新的提交。

传统的答案,可以追溯到 Git 1.5 天,因为子模块 一个 Git 存储库,所以 cd 进入子模块和 git checkout <branchname> 并开始工作。这仍然有效!不过,它有一个明显的缺点:你怎么知道要使用哪个 b运行ch 名称?

在某些情况下,您就是知道。没关系;继续并以这种方式使用它们。但是,如果你想让 superproject 知道,这就是 superproject 的 branch = 设置的用武之地,以及 git submodule update and/or and/or 的参数 submodule.<em>name</em>.update设置(也在超级工程中)进来。记住,这些设置来自超级工程中的.git/config文件, 不是来自子模块本身,并且(通常 3)也不是来自 .gitmodules 文件——但是 .gitmodules 文件内容设置了默认的 .git/config 设置。所以有很多方法可以控制这个配置。

接下来,问题是每个配置 做什么 ,以及您希望如何根据自己的目的进行设置。 the git submodule documentation 中列举了这些(在我看来相当糟糕)。这是我自己对他们的总结的总结,还有额外的评论。

  • checkout: 超级项目中记录的提交将在分离的 HEAD 上的子模块中签出。

    这是默认值,也是我们在上面看到的。

  • rebase: 子模块的当前 b运行ch 将基于超级项目中记录的提交。

    除非您已经进入子模块并在那里做了一些事情,否则这没有用。但是,文档后面还有一个 --remote 选项,这使它更有用。

  • merge: superproject中记录的commit将合并到te 当前 b运行ch 在子模块中。

    rebase 一样,这本身没有用:您需要 --remote 或在执行此操作之前在子模块中完成您自己的工作。

  • 自定义命令:执行带有单个参数(超级项目中记录的提交的sha1)的任意shell命令。

    这个 本身很有用,但需要您在超级项目中做一些 up-front 工作,以设置配置和定义命令。

  • none: 子模块未更新

    这主要用于标记在该特定超级项目的所有其他子模块更新时未更新的子模块。如果你只有一个子模块,这个设置根本没有作用。

到目前为止,我们还没有看到从 .gitmodules 复制到 .git/configbranch 设置有任何用处。正是这个 --remote 选项,在同一文档中进一步描述,讨论了如何使用此设置:

... Instead of using the superproject's recorded SHA-1 to update the submodule, use the status of the submodule's remote-tracking branch.

也就是说,超级项目有一个 gitlink 条目,上面写着 use hash a1b2c3d... 或其他,但不是使用那个 hash ,当 superproject git submodule update 命令四处寻找包含子模块的 Git 存储库时,superproject 命令将在子模块中查找,例如 origin/master。这里的名字master来自那个b运行ch设置,所以设置submodule.<em>name</em>.b运行ch 改为 develop 将使超级项目使用 origin/develop 而不是 origin/master.4

为了使这个有用,超级项目 Git 运行s git fetch 在启动任何这个之前在子模块中。这导致子模块从 its origin Git 带来任何新提交,更新 its origin/master , origin/develop, 等等。这里的假设是 you 自己没有在子模块中做任何工作!您只是在抓取 其他人 origin 存储库中所做的工作,子模块存储库是从中克隆的(哇!)。


3如果 .git/config 中没有设置并且命令行上没有覆盖,则将使用 .gitmodules 中的设置。我认为这是另一个 backwards-compatibility 项。

4这假定 origin/develop 是子模块存储库中与 b运行ch develop 关联的 remote-tracking 名称,即设置正常。


正在准备更新的子模块

如果您打算在自己的子模块中完成自己的工作,none 这对您完全有帮助。 相反,您应该 cd 进入子模块存储库并 运行 git 签出 <em>b运行chname</em>。这将使您脱离分离的 HEAD 并进入给定的 b运行ch,现在您可以正常工作了。像往常一样编写代码、git addgit commit。当子模块中的一切准备就绪后,cd 返回到超级项目。在某些特定的提交中,您的子模块将位于 b运行ch(不是分离的 HEAD 模式)。

如果你只是在接别人的工作,这个git submodule update --remote --checkout或其他任何东西都会git fetch和然后 git checkout origin/master 或子模块中的其他内容。这将使您的子模块在 nob运行ch 上,在分离的 HEAD 模式下,在某些特定的提交上。这可能是您想要的。

在超级项目中使用更新的子模块

无论哪种方式,从超级项目的角度来看,发生的事情是子模块现在处于不同的提交.超级项目 不关心 子模块的 HEAD 是附加的还是分离的;重要的是子模块中的当前提交

现在子模块已在所需的提交上,在超级项目中进行您想要的任何其他更改——例如,可能有一些文件应该使用子模块的一些新功能。完成所需的更改后,git add 任何更新的文件,以及 运行 git add 子模块路径 (没有尾部斜线) :

git add features.ext   # updated to use feature F of submodule sub/S
git add sub/S          # record the new gitlink for sub/S!

这更新了超级项目的索引,所以现在我们不仅有更新的文件 (features.ext),还有子模块的新的正确哈希 ID——更新的 gitlink.现在我们可以像往常一样在超级项目中 运行 git commit:

git commit

这使得我们的新提交,它有一个 gitlink 记录了子模块 sub/S 应该在提交 [=154= 时使用分离的 HEAD 检出的事实] 或者 sub/S 的当前提交实际上是什么。无论是 master 还是 develop 或其他什么,这个新提交都会继续我们在超级项目中检出的任何 b运行ch。

推动

假设我们在 sub/S 中完成了自己的工作,在其 b运行ch devel 上创建提交f37c219...。然后我们在超级项目的 master; 上对我们的超级项目进行了新的提交;有可能它的哈希 ID 是 abcdef1...运行。现在我们有两个带有更新的存储库,我们可以 git push 它们。但是有顺序限制!

假设我们现在推送我们的超级项目:

git push origin master

我们的新提交 abcdef1 进入我们的上游存储库,并且 that Git 的 master 现在命名我们的新提交 abcdef1。我们的新提交表明子模块 sub/S 应该在提交 f37c219 时检出。所以 Fred,在 Fred 的计算机上,运行s git clonegit fetch 或任何它是什么并得到 our commit abcdef1 说“在使用 sub/S 时使用提交 f37c219...”。 Fred 运行s git submodule update 和他的 Git 进入他的 sub/S 并尝试检查 f37c219 并且,哎呀,Fred 没有 f37c219。事实上,只有我们f37c219,因为我们刚刚做到了!

我们最好尽快 cd sub/S 和 运行 git push origin develop。 (请记住,我们在子模块中的 develop 上创建了 f37c219。)这样,当 Fred 尝试访问 f37c219 时,它至少在 某处可用 [=482] =].如果我们 git push 那个 first, then git push origin master 在超级项目中推送 abcdef1 会更好这是指 f37c219。因此,这导致了更新规则 #2:首先按 deepest-submodule 顺序推送子模块。 这样每个超级项目都指向 Fred 或任何人可以访问的提交。

Fred还有一个小痛点

我们在上面介绍了 Fred 作为第一个获取(并合并或变基或以其他方式合并,甚至可能 git pull)我们的超级项目提交的人,该提交指的是新的子项目提交。但是,Fred 在这里代表 任何克隆了我们的超级项目的人。他们都有我们的超级项目,而且他们都 运行 git submodule update --init --recursive,也许作为让他们成为超级项目的克隆命令的一部分,所以他们已经拥有所有子模块。

但是他们还没有子模块中的任何新提交。当他们更新他们的超级项目并告诉他们的 Git 到 git submodule update 时,他们的 Git 将进入他们的子模块并且 找不到正确的提交哈希 。幸运的是,git submodule update 足够聪明,可以为您(或 Fred)运行 git fetch

不过,要使其正常工作,更新者必须在线。这意味着您在连接时必须 运行 git submodule update。如果你总是连接,那没问题,但如果不是,应该有一种简单的方法来预先获取所有子模块。

没有 git submodule fetch,但有一个命令可以解决问题:

git submodule foreach --recursive git fetch

这将 运行 git fetch 在每个子模块中更新它。这样,稍后 git submodule update 与超级项目中的任何提交一起使用,即使您离线并且子模块需要更新也能正常工作。