有没有办法在没有嵌套子模块的情况下添加 git 子模块?

Is there a way to add git submodule without nested submodule?

我有三个存储库。

RepoCommon 作为子模块添加到 RepoTnRepoThRepoTnRepoTh 都是两个不同的存储库,因为在开发的早期就认识到这两个是独立的应用程序。但是现在在开发部分的后期我们意识到 RepoTn 高度依赖于 RepoTh.

作为解决方案,我正在考虑将 RepoTh 作为子模块添加到 RepoTn。但是困扰我的一件事是RepoTh里面的“额外”RepoCommon。如果我这样做,它将基本上具有以下结构:

我想知道当我想将 RepoCommon 作为子模块添加到 RepoTn.

时是否可以从 RepoTh 中删除它

注意:RepoTh 需要继续存在,因为它也有自己的独立存储库。另外,我问这个的另一个原因是如果我可以 remove/ignore RepoCommon 我可以修改项目文件以确保系统使用正确的 RepoCommon 文件夹。如果这有什么不同的话,我正在使用 Visual Studio 2019。也欢迎任何其他关于更好解决方案的建议。

如果可能,您应该添加一个额外的存储库,它将记录这些依赖项:

RepoTnParent
  RepoTn
  RepoTh
  RepoCommon

这样,如果您需要构建 RepoTn,您将加载 RepoTnParent,后者将依次加载正确的版本 RepoTnRepoThRepoCommon.

简短的回答是否定的。这会产生一个问题,您可以使用长答案来解决这个问题,但最好避免这种设置。参见

长答案

你必须记住这里什么是子模块——但在我们到达那里之前,让我们准确定义什么是Git存储库 是。对此有很多注意事项,但简而言之,Git 存储库是 提交的集合 1 每个提交有一个唯一的编号:一个丑陋的大哈希 ID,保留给 that 提交,跨越每个 Git 无处不在。该提交用于保存 Git 在提交时知道的每个文件的快照。2

连同这些存储的文件,这些文件在内部保存为 Git 调用的 blob 对象 ,Git 可以在内部保存 Git调用 gitlink。当我们谈到子模块时,我们稍后会看看 gitlink 的确切形式。

一旦你选择了一些提交——通过它的哈希 ID,你可能 Git 通过分支名称查找例如——在存储库中,你可以 Git 检查 提交(使用git checkout 或,自Git 2.23,git switch)。这会将提交提取到一系列可用(和可修改!)的普通文件中。 Git 复制 3 这些文件到 Git 的 index,这是一个 per-repository 数据结构Git 用于进行 new 提交。4 如果提交包含任何 gitlinks,Git 也会将这些 gitlinks 复制到它的指数.


1最大的警告是存储库所在的.git目录实际上是两个主要数据库,外加一些辅助数据。一个从哈希 ID 映射到内部 Git 对象,其中包括提交,但也包括其他三种对象类型。第二个映射名称——包括分支和标签名称,人类在这里最常使用的名称——到散列 ID,以便可以使用第一个数据库中的散列 ID 人类,例如。

当您克隆存储库时,您的 Git 复制对象数据库 as-is,并使用另一个 Git 的 name-to-hash-ID 数据库构建您自己的(不同) name-to-hash-ID 数据库。没有其他东西——none 的辅助数据,例如钩子、索引或像 CHERRY_PICK_HEAD 这样的特殊伪引用——被复制。所以对象数据库很重要,因为人类使用名称数据库来定义他们的分支概念——这与 Git 的概念不太一样——第二个数据库也很重要。然而,在这里,我们主要关注对象数据库。

2除了快照,提交存储元数据,但这里我们只关心快照本身。

3存储的文件以一种特殊的方式保存,read-only,Git-only文件被压缩的格式和(重要的)de-duplicated。进入索引的实际上是 blob 哈希 ID,因此索引“副本”根本不是真正的副本;但索引还包含文件名,以及使 Git 运行快速的数据。

4索引还有其他作用,但是通过保存每个文件的“副本”以及文件的路径名,索引充当 下一步 提交。当您使用 git add 时,您实际上是在更新 Git 的索引,以便下一次提交不会与当前提交相同。


gitlinks 如何成为子模块

Git 中的一个子模块只是另一个——独立的——Git 存储库,有一个特殊的关系:子模块 Git 存储库由 控制 其他一些 Git 存储库。控制 Git 存储库是 超级项目 ,受控 Git 存储库是子模块。

假定存储库 工作 通过检出提交——这样你就有了可以 使用 的文件,而不是文件是一种特殊的压缩格式,只有 Git 本身可以使用——所有超级项目 Git 本身需要做的,一旦子模块 Git 存储库被克隆到位,就是 运行 git checkout 那个子模块。这就是它的作用:

cd $path
git checkout $hash

为了git checkout子模块中的一些提交,超级项目需要知道两件事:

  • 路径是什么?上面 $path 的值是多少?
  • 哈希 ID 是什么?上面 $hash 的值是多少?

这两件事正是gitlink中的内容。每个提交存储文件。提交也可以存储 gitlinks。文件或 gitlinks 被复制到超级项目 Git 的索引中。 in 索引中的每个条目都由路径名、哈希 ID 和一些其他内部 Git 数据组成。 path-name-and-hash-ID 正是超级项目 Git 所需要的o 执行上述命令。

就这些了……好吧,差不多了。还需要一件事:超级项目 Git 可能 需要 运行 git clone 才能实际创建子模块存储库。 superprojectGit这里需要的信息保存在superproject中的.gitmodulesfile5一旦superprojectGit 有 运行 git clone,但是,不再需要 .gitmodules 数据。6


5请注意,您可以创建一种 half-assed 子模块,其中没有 .gitmodules 子模块的文件条目,但有gitlinks 存储在超级项目的提交中。

6一些git submodule命令仍然会为你更新.gitmodules,一些git submodule命令使用从[=复制的数据20=] 到 .git/config。但是 none 这是 日常使用子模块所必需的 ,一旦它被克隆。


子模块递归和git clone

如果 Git 存储库 A 将 Git 存储库 B 列为子模块(在 A 的 .gitmodules 和 gitlinks-in-commits 中),并且 Git 存储库 B 列出 Git 存储库 A 作为子模块(在 B 的 .gitmodules 和 gitlinks-in-commits 中),任何 自动 克隆和检出提交的操作 and 将子模块定向到 clone-and-checkout 它的 提交可能导致无限递归:

  • A克隆B
  • A 告诉 B:检查提交 CB,其中 CB 有一个 CA[ 的 gitlink =195=]
    • 所以 B 克隆了 A
    • 然后 B 告诉 A:检查提交 CA
      • 所以A克隆了B
      • A 告诉 B:检查提交 CB
        • ...

这里真正的关键是 git clone and/or git checkout 步骤。什么时候超级工程运行git clone?什么时候超级项目将子模块指向 git checkout 一些提交?

如果你保持子模块递归模式关闭,答案是超级项目never 运行s git clone 本身和 never 运行s git checkout 本身。这允许您:

  • 克隆 A
  • 告诉 A 现在克隆 B 并在没有递归的情况下签出一个提交

然后停止。

如果打开递归,A 将克隆 B 并告诉它检查提交,这可能会告诉 B 克隆 A,并且由于递归打开,这将永远继续下去(或者直到你运行 out of disk space 无论如何)。所以只要关闭递归就可以了——但是每个使用这些存储库的人都必须知道这样做。