这通常是一个难题。有一些特定的情况,或者应用子模块内容的退化方式,这使得它更容易。一种折衷方案(可能不够好也可能不够好)是简单地将两个提交历史合并到一个存储库中,然后使用git filter-branch 或仅使用自动化git replace 进行一些稍微可怕的转换(尽管使用或滥用, git replace 这样可能会导致性能问题)。
这是基本情况,即作为心理工具,在您考虑概括问题之前需要了解的内容。每个存储库都包含一个提交图:提交的DAG,其中通过分支名称找到并保存了图中的各种入口点。超级项目的提交在使用子模块的每个提交中都有对子模块提交之一的引用。这些引用在“树”对象中,作为 gitlink 类型的条目。 Git 在保留提交时实际上并没有检查它们,因为它们被假定为识别某些 other 存储库(子模块)中的提交。
您可以轻松地使用git fetch 将整个子模块的图提取到超级项目存储库中,将子模块的分支名称更改为超级项目中的不同名称。 (git fetch 的默认值是生成远程跟踪名称,但有点偷偷摸摸,您可以轻松地使用替代命名空间。对于我提出的解决方案,远程跟踪名称无论如何都可以。)结果,虽然,只是你有两个断开连接的 DAG。超级项目提交仍然只有带有 gitlink 条目的树,这些条目引用另一个 DAG 中的提交。这些 gitlink 条目不会保留提交 reachable,因此您必须保留两组名称。除了将所有提交都包含在一个存储库数据库中之外,这实际上根本没有任何改进(而且可能会更糟,因为现在很难使用)。
这是一般 问题:Git 存储的是(是?)这些提交。没有单独的项目可以称为“历史”; Git 存储库中的历史记录是(是?)存储库中的提交。如果我们绘制提交,我们可以直观地看到问题。让我们在超级项目中将其简化为只有五个提交,A 到 E。大写字母代表实际的哈希 ID(对人类无用):
A--B--C <-- master
\
D--E <-- dev
现在让我们在子项目中放置六个提交,使用小写字母,因为它是子项目:
a--b--c--d <-- master
\
e--f <-- issue213
一些超级项目提交——可能是所有提交,但为了简单起见,我们只说C 和E——在其中包含对一些子项目提交的引用,所以如果我们将子模块的所有提交拉到超级项目中,使用名称sub/* 来记住分支提示,我们得到:
A--B--C <-- master
\ :
D÷-E <-- dev
: :
: :
: :
: :
a--b--c--d <-- sub/master
\
e--f <-- sub/issue213
假设我们现在,以某种方式,将提交 C(其 gitlink 指向 b)和 E(其 gitlink 指向 d)替换为其树具有实际、直接引用树对象的提交提交b 和e。我们将这些提交称为C' 和E'。这在 Git 中在技术上是可行的——我们只需使用我们想要的树进行新的提交 C' 和 E',它们分别使用 b 和 d 中的树,然后更改名称 master 和 @987654350 @ 表示提交 C' 和 E'。如果我们删除 sub/* 名称,我们有这个:
A--B--C' <-- master
\
D--E' <-- dev
如果我们现在git checkout master,我们将得到一个很好的工作树,其中包含原始C 中的内容加上来自子模块的内容,从其提交b 获得从图表中可以看出,使用了原来的C。
同样,如果我们现在 git checkout dev,我们将得到一个很好的工作树,其中包含原始 E 中的内容以及从其提交 d 获得的子模块中的内容。
这个新修改的存储库中的树包含您通过查看C-and-submodule 或E-and-submodule 获得的快照的所有源。但是 commits 是 in 子模块,即 d 导致回到 c 导致回到 b 导致回到 a 的历史,加上整个 issue213 分支,由 f 和 e 和 c 组成......好吧,那些提交已经消失了!没有什么可以代表他们了。
此外,没有地方可以插入它们。在包含提交A 到E(全部大写)的图表中,在哪里提交a 到f(全部小写)适合?唯一的答案是“无处”:他们没有地方可以去。
现在,在特定情况下,我们可以发明一个答案。我们可以在现有提交之间插入 new 提交,以便新提交在更新子模块文件的同时保留超级项目的文件。每当存在“适合”超项目图的拓扑排序的子模块图的拓扑排序时,这都是实用的。 (如果有多个子模块,我们需要对所有图的并集进行完整的拓扑排序。)不保证这种情况存在,很容易画出不存在的情况:
A--B--C <-- master
: :
: :
:
: :
: :
a--b--c <-- sub/master
这里,superproject commit A 是指子项目中的last commit,而 superproject commit C 是指子项目中的first commit。这些图拓扑是不可组合的。1 但您的拓扑可能是这种情况,在这种情况下,您可以根据需要插入提交节点,如果您想组成一个新图作为适当的超集。据我所知,没有任何程序可以做到这一点。
1我不确定“可组合”是否是一个好的术语,但我没有时间搜索文献。我的意思是组合 DAG 可能会导致循环,我将此类存储库称为“不可组合”。例如,另请参阅Efficient algorithm for merging two DAGs。
使用可组合的子模块完成更复杂的工作
您将不得不编写一些代码。 ? 这很重要,需要一点图论。不是特别复杂,但我这里肯定不会做。
如果可以接受截断的历史记录,则做更简单的工作
在上面的示例中,更简单的工作包括将提交 C 替换为 C' 和 E 替换为 E',它是可自动化的:遍历所有提交,找到它们的子模块 gitlink,并使用 git replace用使用子模块树的树对象替换具有子模块的树对象。这实际上替换了树对象,而不是提交对象,因此历史实际上仍然是以前的方式,但是您现在将拥有非常大的替换对象集合。此外,克隆存储库不会克隆替换对象,所以现在是时候重写所有提交了,使用git filter-branch。
我没有像这样使用git replace 的简便方法,但是您可能希望通过将GIT_EDITOR 变量设置为可以查找和替换gitlink 条目的脚本来自动化git replace --edit。 (编写这样的脚本会有点乏味,但技术上并不困难。)
由于git filter-branch 尊重替换,2 并且不需要其他更改,您只需运行git filter-branch --tag-name-filter cat -- --branches --tags 即可执行所有提交替换。 (注意:在您专门为尝试替换和过滤分支而制作的克隆上执行此操作,以便在搞砸时可以重新开始。)然后您可以删除所有替换引用 (git for-each-ref --format='delete %(refname)' | git update-ref --stdin)因为它们不再需要,现在只是让 Git 变慢。
2嗯,除非以git --no-replace-objects filter-branch 运行,否则它确实如此。