【问题标题】:Convert a git submodule to a regular directory and preserve the history in the main tree?将 git 子模块转换为常规目录并保留主树中的历史记录?
【发布时间】:2018-09-07 14:05:00
【问题描述】:

我有一个由许多子模块组成的项目。但是,事后看来,其中一些子模块不应该是子模块,因为它们并不意味着或永远不会在另一个项目中使用,而且我偶尔会在它们之间传输代码。这个项目兼作子模块的实验,所以我有点疯狂。

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

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

【问题讨论】:

标签: git git-submodules


【解决方案1】:

如果您只想保留每个子模块的单个分支的历史记录,使用 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 updategit status 仔细检查。

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

如果你需要整合几个子模块的历史,我建议将所有的删除操作放在第一个提交中,将所有的整合操作放在第二个提交中。在这种情况下,您需要通过其他方式记住获取的 HEADS。


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

【讨论】:

  • 顺便说一句,查看git filter-repo 以获得更复杂的提交重写。但请注意,它是一种非常锋利的工具:强大、快速且危险。它重写了一个完整的 Git repository,因此您必须处理一个 新鲜且独立的克隆。
【解决方案2】:

我没有太多使用子模块的经验,但我会这样做:

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

也许不是最好的方法,但如果我想将不同的项目引入我的项目,同时保持独立,我会这样做。

** 这可能吗?我肯定需要在子模块等方面获得更多实践经验。

【讨论】:

    【解决方案3】:

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

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

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

    这是一般 问题: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 中的树,然后更改名称 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 获得的快照的所有源。但是 commitsin 子模块,即 d 导致回到 c 导致回到 b 导致回到 a 的历史,加上整个 issue213 分支,由 fec 组成......好吧,那些提交已经消失了!没有什么可以代表他们了。

    此外,没有地方可以插入它们。在包含提交AE(全部大写)的图表中,在哪里提交af(全部小写)适合?唯一的答案是“无处”:他们没有地方可以去。

    现在,在特定情况下,我们可以发明一个答案。我们可以在现有提交之间插入 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 运行,否则它确实如此。

    【讨论】:

      猜你喜欢
      • 2021-12-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-06-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-31
      相关资源
      最近更新 更多