将git子模块转换为常规目录,并将历史记录保留在主树中吗?

时间:2018-09-07 14:05:00

标签: git git-submodules

我有一个包含许多子模块的项目。但是,事后看来,其中一些子模块不应该是子模块,因为它们并非意味着或永远不会在另一个项目中使用,并且我偶尔会在它们之间传递代码。这个项目是子模块中的实验的两倍,所以我对此有些疯狂。

我想知道是否有一种方法可以将子模块转换为常规目录,保持更改历史记录,但重写主项目的历史记录,以便将它们视为常规目录。

我已经看到了有关子树合并的内容,但是我希望找到一种重写提交的方法,以便文件路径以子模块的前缀。

3 个答案:

答案 0 :(得分:1)

通常这是一个难题。在某些情况下,或者应用子模块内容的简并方式使之变得更容易。一种折衷方案(可能不够,也可能不够好)是简单地将两个提交历史记录合并到一个存储库中,然后使用git filter-branch或仅使用自动化git replace进行一些稍有改动的转换(尽管如此使用或滥用git replace可能会导致性能问题。

这是基本情况,即在考虑推广问题之前,您需要了解的一些心理工具。每个存储库都包含一个提交图:DAG个提交,在图中找到了各种入口点,并通过分支名称保留了这些入口点。在使用子模块的每个提交中,超级项目的提交都具有对子模块的提交之一的引用。这些引用作为“ gitlink ”类型的条目位于“树”对象中。关于保留提交,Git实际上不会对其进行检查,因为假定它们是在某些 other 存储库(子模块)中标识提交的。

您可以轻松地使用git fetch将整个子模块的图提取到超级项目存储库中,从而将子模块的分支名称更改为超级项目中的不同名称。 (git fetch的默认设置是生成远程跟踪名称,但有些偷偷摸摸,您可以轻松地使用其他名称空间。对于我建议的解决方案,无论如何远程跟踪名称都是可以的。)但是,仅仅是您有两个断开连接的DAG。超级项目提交仍然只有带有gitlink条目的树,这些条目引用另一个DAG中的提交。这些gitlink条目将不保留提交reachable,因此您必须保留两组名称。除了将所有提交都包含在一个存储库数据库中之外,这根本没有任何改善(并且可能会变得更糟,因为现在很难使用它。)

这是 general 问题:Git存储的是这些提交。我们没有单独的项目可以称为“历史”; Git存储库中的历史记录(是?)存储库中的提交。如果我们绘制提交,我们可以从视觉上看到问题。让我们在超级项目中将其简化为从AE的五个提交。大写字母代表实际的哈希ID(对人类没有用):

A--B--C   <-- master
    \
     D--E   <-- dev

现在,由于它是子项目,所以我们使用小写字母在子项目中放置六个提交:

a--b--c--d   <-- master
       \
        e--f   <-- issue213

一些超级项目提交(也许是全部),但为简单起见,我们只说CE,在其中包含了对某些子项目提交的引用,因此,如果我们将所有子模块的提交使用名称sub/*来记住分支提示,从而提交到超级项目中,我们得到以下信息:

A--B--C   <-- master
    \ :
     D÷-E   <-- dev
      : :
     :  :
    :    :
   :     :
a--b--c--d   <-- sub/master
       \
        e--f   <-- sub/issue213

假设我们现在以某种方式用树的实际提交替换提交C(其gitlink到b)和E(其gitlink到d) ,直接引用树对象以提交be。我们将这些提交称为C'E'。从技术上讲,这在Git中是可能的-我们只对想要的树进行新提交C'E',分别使用bd中的树,然后进行更改名称masterdev来引用提交C'E'。如果我们删除sub/*名称,我们将拥有:

A--B--C'  <-- master
    \
     D--E'  <-- dev

如果现在使用git checkout master,我们将获得一个很好的工作树,其中充满了原始C plus 子模块中的内容,该子模块是从其提交中获得的正如我们从图中看到的那样,原始b使用的C

类似地,如果现在git checkout dev,我们将得到一个很好的工作树,其中充满了原始E中的内容以及子模块的提交d中的内容。

这个新的经过修改的存储库中的包含通过检出C-and-submodule或E-和而获得的快照的所有源-子模块。但是 commits 是子模块中的,即d导致历史回到c导致历史回到b回到a,再加上整个issue213分支,其中包括f引回到e引回到c……好吧,这些提交消失了!没有任何东西可以代表它们了。

此外,没有可以插入它们的地方。在包含AE(全部为大写)的图形中,哪里af(全部为小写) fit?唯一的答案是“无处”:他们不能去任何地方。

现在,在特定情况下,我们可以发明一个答案。我们可以在现有提交之间插入 new 提交,以便新的提交在更新子模块文件时将超级项目的文件保留在原位。只要子模块图的拓扑类型“适合”超级项目图的拓扑类型,这就是实用的。 (如果有多个子模块,则需要所有图的并集的完整拓扑排序。)不能保证存在这种情况,并且很容易得出不存在这种情况的情况:

A--B--C   <-- master
 :   :
  : :
   :
  : :
 :   :
a--b--c   <-- sub/master

在这里,超级项目提交A引用子项目中的 last 提交,而超级项目提交C引用子项目中的 first 提交。子项目。这些图拓扑是不可组合的。 1 但是,可能是您的拓扑是这种情况,在这种情况下,您可以根据需要插入提交节点,如果您想组成一个新的图来充当适当的超集。我不知道要执行此操作的程序。


1 我不确定“ composable”是否是一个好词,但是我没有时间进行文献检索。我的意思是,合并DAG可能会导致周期,因此我将此类存储库称为“不可组合的”。例如,另请参见Efficient algorithm for merging two DAGs


使用可组合的子模块完成更复杂的工作

您将不得不编写一些代码。这是不平凡的,需要一些图论。它不是特别复杂,但是我绝对不会在这里做。

如果可以接受截断的历史记录,则做得简单

在上面的示例中,更简单的工作是可以自动化的:它可以遍历所有提交,找到它们的提交,然后将提交C替换为C',将E替换为E'。子模块gitlinks,并使用git replace将具有子模块的树对象替换为使用子模块树的树对象。实际上,这将替换树对象而不是提交对象,因此历史记录确实仍然是以前的样子,但是现在您将拥有大量替换对象。此外,克隆存储库不会克隆替换对象,因此现在该使用git filter-branch重写所有提交了。

我没有像这样使用git replace的便捷方法,但是您可能希望通过将git replace --edit变量设置为一个可以找到并替换gitlink条目。 (编写这样的脚本会有些乏味,但在技术上并不困难。)

由于GIT_EDITOR尊重替换, 2 并且不需要其他更改,因此您只需运行git filter-branch即可执行所有提交替换。 (请注意:在专门为替换和筛选分支进行实验的克隆上执行此操作,以便将其弄乱后可以重新开始。)然后,您可以删除所有替换引用({{1} }),因为它们不再需要了,现在只是让Git变慢了。


2 好吧,除非以git filter-branch --tag-name-filter cat -- --branches --tags运行,否则它会起作用。

答案 1 :(得分:0)

我在处理子模块方面经验不足,但这就是我要做的事情:

  • 从项目中删除子模块**。将“原始子模块”存储库添加为项目的远程目录并进行获取。
  • 将要带入项目的任何分支合并。如果我想将其他项目中的文件保存到主项目的单独目录中,则可能需要检出子模块分支(不再是子模块,现在是真正的远程分支),将文件重命名为目录是我的意思(这样它就不会与主项目中的任何内容冲突),然后将这个新修订版本合并到我的主项目中。

也许不是最好的方法,但是如果我想将一个不同的项目引入到我的项目中,同时又要使事情分开,那将是我要做的事情。

**甚至可能吗?我绝对需要获得更多有关子模块之类的动手经验。

答案 2 :(得分:0)

如果只想保留每个子模块的单个分支的历史记录,则使用git subtree相当容易:

git fetch <path/to/submodule> HEAD
git rm <path/to/submodule>
git commit -m "Prepare to integrate Git submodules' history into repository"
git subtree add --prefix=<path/to/submodule> FETCH_HEAD 

这将集成子模块当前已检出修订版的历史记录。确保之前处于清洁状态,例如运行git submodule update并使用git status仔细检查。

您将获得两个提交:第一个删除子模块,第二个将先前的历史记录(现在存储在FETCH_HEAD中)整合到存储库中。没有简单的方法(至少我不知道)通过“原子”提交来做到这一点。为此,您需要弄乱Git的管道命令。

如果需要集成多个子模块的历史记录,建议将所有删除操作放入第一个提交中,并将所有集成操作放入第二个提交中。在这种情况下,您需要通过其他方式记住提取的HEADS。


注意: 尽管git subtree位于上游Git的./contrib之内,但自v1.9.1(2014年3月)以来,它似乎(至少)在Debian上可用。