【问题标题】:Merge two distinct git repositories by interlacing commits通过交错提交合并两个不同的 git 存储库
【发布时间】:2019-09-16 15:30:59
【问题描述】:

我们有两个并行发展的存储库:一个用于我们项目的代码,另一个用于该项目的测试。我想将这两个存储库合并到一个存储库中,这样当我回顾历史时,我仍然拥有 both 目录结构。

假设我们当前的结构如下,其中projecttests 是两个独立的git 存储库:

project
    /src
    /include
tests
    /short
    /long

我想最终得到一个 git 存储库,它有两个目录 projecttests

我不能使用this answerthis onethis site 中描述的技术简单地合并这两个存储库:它们会导致存储库在合并之前具有两个不同的历史记录,并且在签出过去的提交时,您有 srcinclude,或 shortlong,但您没有当时出现的所有四个。

如果我签出 4 个月前在 project 创建的提交,我希望看到 project/srcproject/include 出现在此提交中,但我也希望有 tests/short 和 @ 987654337@,因为它们同时位于(然后是分开的)test 存储库中。

我了解两个存储库之间的提交顺序仅取决于时间,并且可能不是很精确。但这对我来说已经足够了。当然,我知道我不能保留每个 repo 的原始 git id。没关系,因为这两个 repos 实际上是从另一个 RCS 新导入的,因此没有任何地方记录过 git id。

应该可以从每个存储库中逐一签出所有提交,按跨存储库的时间排序,并提交生成的文件。是否已经有工具可以做到这一点?

【问题讨论】:

    标签: git


    【解决方案1】:

    编辑:对于一种基于日期的方法,它使这变得非常容易,但假设两个存储库之一将“控制”来自另一个存储库的提交,请参阅jthill's answer。您最终会得到与“项目”历史完全匹配的提交历史,可能会压缩一些“测试”历史。如果您需要为 both 组历史记录添加前缀,或者想要将它们交错(例如,需要对同一个“项目”提交进行两个不同的“测试”更新),则下面的答案更合适。


    phd's answer 很好,但如果我自己这样做并且想让它真正整洁干净,我会使用不同的方法。

    如果两个存储库的树不重叠,当然可以做到这一点——通过绕过通常的 Git 机制,直接使用底层的git read-tree 命令,您可以自动执行它。 (这就是 VonC's recent comment 拒绝我声称 Git 和 Mercurial 非常相似的说法是正确的:如果您绕过顶级 Git 命令,您会得到一些在 Mercurial 中几乎无法轻易获得的东西。)

    就像在phd's answer 中一样,您可以通过git fetch 组合两个存储库提交数据库来启动此过程。 (您可以在第三个存储库中执行此操作,我建议这样做,因为如果您决定要调整某些参数,或者通过将存储库 A 添加到存储库 B 或将存储库 B 添加到repo A.)​​ 但在那之后,一切都不同了。

    您现在有两个不相交的提交 DAG:

            D--...--K
           /         \
    A--B--C           M--N   <-- repoA/master
           \         /
            E--...--L
    
    O--P--Q--...--Z   <-- repoB/master
    

    (如果 repoA 和 repoB 都有多个分支提示,请绘制更合适的提交简化图。)

    您的下一步是使用git rev-list --topo-order --reverse 和您喜欢的任何其他排序选项枚举两个不相交的 DAG 中的每一个中的所有提交。何时以及是否需要 --topo-order 取决于拓扑结构和其他排序信息,但通常您希望在其任何子项之前列出父提交。

    鉴于这两个提交哈希 ID 的线性化列表,您现在遇到了困难的部分:构建您希望提交的新组合树的图。每个 new 提交都将通过组合两个旧图表中的每一个中的一个提交来进行。如果其中一张图很复杂(如上面的 repoA),带有分支和合并,而一张不是(如上面的 repoB),这可能特别棘手。

    我为此做了自己的设置,其中有一个非常简单的图表:

    A--B   <-- A/master
    
    O--P   <-- B/master
    

    在我的简化设置中,我想让我的新主人的第一次提交是提交C,它结合了AO 的树:

    C   <-- master
    

    然后,作为我对master 的第二次提交,我想组合AP(不是AO,也不是BO) ,作为我的最后一次提交,BP 的组合,所以我最终得到:

    C--D--E   <-- master
    
    with:
        C = A+O
        D = A+P
        E = B+P
    

    所以,这里我们在一个新的空存储库中,除了我们在项目 A 和 B 中读取:

    $ git log --all --graph --decorate --format='%h%d %s' --name-status | sed '/^[| ] $/d'
    * 7b9921a (B/master) commit-P
    | A B/another
    * 51955b1 commit O
      A B/start
    * 69597d3 (A/master) commit-B
    | A A/new
    * ff40069 commit-A
      A A/file
    

    (我不小心没有对提交 O 进行连字符,而是对所有其他文件进行了连字符。在这种情况下,sed 是为了删除一些对阅读没有帮助的空行。)

    $ git status
    On branch master
    
    No commits yet
    
    nothing to commit (create/copy files and use "git add" to track)
    

    现在我们构建新的提交,一次一个,使用git read-tree 填充索引以进行提交。我们从一个空索引开始(我们现在有):

    $ git status
    On branch master
    
    No commits yet
    
    nothing to commit (create/copy files and use "git add" to track)
    

    我们希望我们的第一个提交结合AO,所以现在让我们将这两个提交读入索引。如果我们必须为 A 中的树添加前缀,我们可以在这里这样做:

    $ git read-tree --prefix= ff40069
    $ git ls-files --stage
    100644 7a1c6130c652b6ea92f4d19183693727e32c9ac4 0       A/file
    $ git read-tree --prefix= 51955b1
    $ git ls-files --stage
    100644 7a1c6130c652b6ea92f4d19183693727e32c9ac4 0       A/file
    100644 f6284744575ecfc520293b33122d4a99548045e4 0       B/start
    

    我们现在可以进行我们需要的提交了:

    $ git commit -m combine-A-and-O
    [master (root-commit) 7c629d8] combine-A-and-O
     2 files changed, 2 insertions(+)
     create mode 100644 A/file
     create mode 100644 B/start
    

    现在我们需要进行下一次提交,这意味着我们需要在索引中建立正确的树。为此,我们首先必须将其清理干净;否则下一个 git read-tree --prefix 将失败并抱怨文件重叠和 Cannot bind. 所以现在我们清空索引,然后读取提交 A 和 P:

    $ git read-tree --empty
    $ git read-tree --prefix= ff40069
    $ git read-tree --prefix= 7b9921a
    

    如果您愿意,可以再次使用git ls-file --stage 检查结果:

    $ git ls-files --stage
    100644 7a1c6130c652b6ea92f4d19183693727e32c9ac4 0       A/file
    100644 d7941926464291df213061d48784da98f8602d6c 0       B/another
    100644 f6284744575ecfc520293b33122d4a99548045e4 0       B/start
    

    无论如何,它们现在可以作为新的提交提交:

    $ git commit -m 'combine A and P'
    [master eb8fa3c] combine A and P
     1 file changed, 1 insertion(+)
     create mode 100644 B/another
    

    (您现在可以看到我如何以不一致的连字符结尾:-))。最后,我们通过清空索引、读取两个期望的提交 (B+P) 并提交结果来重复该过程:

    $ git read-tree --empty
    $ git read-tree --prefix= A/master
    $ git read-tree --prefix= B/master
    $ git ls-files --stage
    100644 7a1c6130c652b6ea92f4d19183693727e32c9ac4 0       A/file
    100644 8e0c97794a6e80c2d371f9bd37174b836351f6b4 0       A/new
    100644 d7941926464291df213061d48784da98f8602d6c 0       B/another
    100644 f6284744575ecfc520293b33122d4a99548045e4 0       B/start
    $ git commit -m 'combine B and P'
    [master fad84f8] combine B and P
     1 file changed, 1 insertion(+)
     create mode 100644 A/new
    

    (我在这里使用符号名称来获取最后两个提交,但是来自 git rev-list 的哈希 ID 当然可以很好地工作。)我们现在可以看到三个提交,都在 master

    $ git log --decorate --oneline --graph
    * fad84f8 (HEAD -> master) combine B and P
    * eb8fa3c combine A and P
    * 7c629d8 combine-A-and-O
    

    现在可以安全地删除 A/masterB/master 引用(以及两个遥控器)。有一个特点:因为我们直接在索引中完成了所有工作,而没有打扰工作树,工作树仍然是完全空的:

    $ ls
    $ git status -s
     D A/file
     D A/new
     D B/another
     D B/start
    

    最后要解决这个问题,我们应该运行git checkout HEAD -- .

    $ git checkout HEAD -- .
    $ git status -s
    $ git status
    On branch master
    nothing to commit, working tree clean
    

    如何编写自己的自动化脚本

    在实践中,您可能希望使用git write-treegit commit-tree 而不是git commit 来进行新的提交。您将编写一个小脚本(使用您喜欢的任何语言)来运行 git rev-list 以收集要组合的提交的哈希 ID。脚本必须检查这些提交——例如,通过查看作者身份和日期,或文件内容,或其他任何东西——来决定如何交织提交。然后,在决定了交织以及要提供哪些分支和合并结构之后,脚本可以开始重复执行这些步骤的过程:

    • 清空索引。
    • 从 repo-A 的子图中的提交中提取树,使用任何合适的 --prefix 选项 - 在您的情况下,这是 --prefix=,即空字符串,但在其他情况下它将是带有尾部斜杠的目录名称)。
    • 从 repo-B 的子图中的提交中提取树,并使用另一个适当的 --prefix,以便 AB 的条目之间没有冲突。
    • 使用git write-tree 写入树。它的输出是下一步的树哈希 ID。
    • 使用git commit-tree 和适当的-p 参数来设置新提交的父级。为它提供适当的(组合或其他)提交消息文本。使用环境变量GIT_AUTHOR_NAMEGIT_AUTHOR_EMAILGIT_AUTHOR_DATEGIT_COMMITTER_NAMEGIT_COMMITTER_EMAILGIT_COMMITTER_DATE 来控制作者和提交者的名称和日期。 git commit-tree 的输出是哈希 ID,它是一些后续提交的父级。

    当整个事情完成后,为任何特定分支或一组分支所做的 last 提交是进入这些分支的哈希 ID,因此您现在可以运行:

    git branch <name> <hash>
    

    对于每个这样的哈希 ID。

    【讨论】:

    • 我更多地暗示了“仅拉取与共享推送”:hgbook.red-bean.com/read/…。随着早期的变基和拉取请求,GitHub 模型流行起来,而 BitBucket 模型(最初基于 Subversion,然后是 Mercurial)迎头赶上。我仍然记得我与 Ry4an(他的真名!)关于 rebase 和 Mercurial 不可磨灭的变更集的辩论! (stackoverflow.com/a/2672489/6309)
    • @VonC:比 rebase 更好的是 Mercurial 的“evolve”扩展。不幸的是,这仍然不在官方 Hg 中(甚至不是捆绑扩展)。在 rebase 和 histedit 成为捆绑扩展之前,Mercurial 有点不足:你可以移植和剥离,但这非常粗糙。
    【解决方案2】:

    [假设所有project 内容都在srcinclude 中,所有tests 内容都在shortlong 中,]

    如果我签出 4 个月前在项目中创建的提交,我希望看到 project/srcproject/include 出现在此提交中,但我也希望看到 tests/shorttests/long因为它们同时在(然后是单独的)测试存储库中。 […]

    是否已经有工具可以做到这一点?

    有,它被命名为git filter-branch。到目前为止,最简单的实现是遍历project 历史并查找“the”对应的tests 提交的内容,这是一个草图:

    git init junk
    cd junk
    git remote add project /path/to/project
    git remote add tests /path/to/tests
    git remote update
    
    git filter-branch --index-filter '
            mydate=`git show -s --date=raw --pretty=%ad $GIT_COMMIT`
            thetest=`git rev-list -1 --before="$mydate" --remotes=tests`
            [[ -n $thetest ]] && git read-tree --prefix= $thetest
    ' -- --remotes=project
    

    如果你的“测试”历史有数千次提交,这会变得很慢,如果你在谈论 linux repo 或那种规模的东西,那么预先生成一个按日期排序的测试列表并逐步完成它会更便宜.

    【讨论】:

    • get remote update 之后似乎有必要添加git commit --allow-empty -m "Empty commit before filter-branch"。否则 git filter-branch 会出现 fatal: Needed a single revision 错误。
    • 这个方法的效果可以这样描述:重写project中的提交,使得它们也包含自上次提交以来在test中发生的更改。换句话说,test 中的提交被压缩,而更改添加到project 中的提交。我更愿意将来自test 的提交与project 中的提交分开。另一方面,到目前为止,单个命令比所有其他答案要简单得多...
    • 如果您想保留测试历史结构,最简单的方法是将提交添加为子模块,而不是使用读取树 git update-index --cacheinfo 160000,$thetest,tests
    【解决方案3】:

    我认为您应该将两个存储库结合起来创建 2 个分支(git fetch 没有合并)。然后交互式地变基一个分支,在每次提交时停止并在当前分支中执行git cherry-pick 相应的提交。然后继续交互式变基到下一个提交(这会保存“编辑”的提交而不进行修改)。

    也许这甚至可以自动化。您可能可以使用git rebase --interactive -x 在每次提交后执行git cherry-pick,而不是交互式rebase 和手动挑选。问题是如何找出对樱桃挑选的承诺。我认为应该是second-branch~count。在编辑 rebase-todo 文件时,可以在交互式 rebase 之前编辑计数。

    【讨论】:

      猜你喜欢
      • 2010-09-30
      • 2022-11-16
      • 1970-01-01
      • 2015-09-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多