通过交错提交合并两个不同的git仓库

6
我们有两个仓库,分别存放着项目代码和相关测试。我希望将这两个仓库合并成一个,这样当我回到历史记录时,仍然能够看到两个目录结构。
假设我们当前的结构如下,其中projecttests是两个单独的git仓库:
project
    /src
    /include
tests
    /short
    /long

我想最终得到一个git仓库,其中包含两个目录projecttests。但是我不能简单地使用这个答案这个答案或者这个网站中所描述的技术来合并这两个仓库:它们在合并之前有两个不同的历史记录,并且当检出过去的提交时,你只能看到其中一个,要么是srcinclude,要么是shortlong,但你无法同时看到它们在那个时间点上的所有四个。
如果我检出了4个月前在project中创建的提交,我希望能看到project/srcproject/include以及(先前独立的)test仓库中的tests/shorttest/long,就像它们在那个时间点上一样。
我知道这两个仓库之间的提交顺序只取决于时间,可能不是非常准确。但这对我来说已经足够了。当然,我知道我不能保留每个仓库原有的git id。因为这两个仓库实际上都是从另一个版本控制系统中导入的,所以根本没有任何git id被记录在任何地方。
理论上说,逐个检出每个仓库中的所有提交,并按仓库之间的时间顺序进行排序,再提交结果文件应该是可行的。是否已经有工具可以实现这个操作?
3个回答

3

编辑:如果想要采用基于日期的方法来处理这个问题,这样做会相当简单,但是前提是两个仓库中的一个将“控制”另一个仓库的提交。详见jthill的回答。你最终得到的提交历史将完全匹配“项目”历史,可能会压缩一些“测试”历史。下面的答案更适合在需要为两个历史记录集添加前缀或者想要交错它们的情况下使用(例如需要两个不同的“测试”更新来针对同一个“项目”提交)。


phd的答案很好,但如果我自己做并想使其更加整洁和清晰,我会采用不同的方法。

如果两个仓库的没有重叠,那么肯定可以实现这一点,并且通过绕过常规的Git机制,并直接使用底层的git read-tree命令,你可以自动化这个过程。(这也是VonC最近的评论,拒绝了我关于Git和Mercurial非常相似的说法是正确的:如果你绕过顶级的Git命令,你会得到在Mercurial中很难得到的东西。)

就像phd的答案中一样,你将通过git fetch合并两个仓库的提交数据库开始这个过程。(你可以在第三个仓库中进行此操作,我建议这么做,因为这样如果你决定调整某些参数,或者将仓库A添加到仓库B,或者将仓库B添加到仓库A,那么重新启动该过程会更容易。)但是,在此之后,一切都会发生变化。

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

        D--...--K
       /         \
A--B--C           M--N   <-- repoA/master
       \         /
        E--...--L

O--P--Q--...--Z   <-- repoB/master

(如果 repoA 和 repoB 都有多个分支尖端,请绘制与其提交相关的任何简化图形。)
你需要做的下一步是枚举这两个不相交DAG中所有提交,可以使用 git rev-list --topo-order --reverse 命令和其他喜欢的排序选项。是否需要使用 --topo-order 取决于拓扑结构和其他排序信息,但通常你希望父提交在其子提交之前列出。
得到这两个线性化的提交哈希ID列表后,你现在需要完成困难的部分:构建你想要提交的新组合树的图形。每个新提交都将通过结合两个旧图中的一个提交来完成。如果其中一个图具有复杂的分支和合并(如上面的repoA),而另一个则没有(如上面的repoB),这可能会特别棘手。
我为此制定了自己的设置,其中我有一个非常简单的图形:
A--B   <-- A/master

O--P   <-- B/master

在我的简化设置中,我想要在新主分支上进行的第一个提交是将AO的树合并为提交C
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。
  • 使用适当的-p参数对git commit-tree进行设置,以设置新提交的父项。 提供适当的(合并或其他)提交消息文本。 使用环境变量GIT_AUTHOR_NAMEGIT_AUTHOR_EMAILGIT_AUTHOR_DATEGIT_COMMITTER_NAMEGIT_COMMITTER_EMAILGIT_COMMITTER_DATE来控制作者和提交者名称和日期。 git commit-tree的输出是哈希ID,它是某个后续提交的父项。

整个过程完成后,任何特定分支或一组分支的最后一个提交都是进入这些分支的哈希ID,因此现在您可以运行:

git branch <name> <hash>

对于每个这样的哈希ID。


我更多地是在暗示“仅拉取与共享推送”的问题:http://hgbook.red-bean.com/read/collaborating-with-other-people.html#id372641。通过变基和提前拉取请求,GitHub 模型得到了广泛应用,而 BitBucket 模型(最初基于 Subversion,然后是 Mercurial)则在追赶中。我仍然记得我与 Ry4an(他的真名!)关于变基和 Mercurial 不可消除的 changesets 的辩论!(https://dev59.com/NnE85IYBdhLWcg3wqVX5#2672489) - VonC
@VonC:比rebase更好的是Mercurial的“evolve”扩展。不幸的是,它仍然没有成为官方Hg的一部分(甚至不是捆绑扩展)。在rebase和histedit成为捆绑扩展之前,Mercurial有点不足:你可以graft-and-strip,但那非常粗糙。 - torek

3
[假设所有project的内容都在srcinclude目录中,而所有tests的内容都在shortlong目录中,] 如果我要检出一个创建于4个月前的项目提交,我想看到project/srcproject/include与该提交中相同,但我也想看到tests/shorttests/long与当时单独的测试存储库中的内容相同。[…] 已经有这样的工具了吗?
答案是有的,它叫做git filter-branch。最简单的实现方式是遍历project的历史记录,查找“对应”的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仓库之类的大规模项目,预先生成按日期排序的测试列表并逐步处理可能更加经济实惠。


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

2
我认为你应该合并这两个存储库,并创建 2 个分支(不进行合并的 git fetch)。然后交互式地重新制作一个分支,在每个提交处停止并将相应的提交使用 git cherry-pick 到当前分支中。然后继续交互式重新制作到下一个提交(这样可以保存“编辑”的提交而没有修改)。
也许甚至可以自动化。你可能可以使用 git rebase --interactive -x 来代替交互式重新制作和手动挑选,执行 git cherry-pick 在每个提交之后。问题是如何找出要挑选的提交。我认为应该是 second-branch~count。在编辑重新制作待办事项文件时,可以编辑计数。
"Original Answer"翻译成"最初的回答"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接