真正地平滑进行git合并

12

在StackOverflow上有一些与“展平合并”有关的问题,通常的答案是使用“git rebase”,但是这些答案忽略了一个至关重要的点——提交顺序。

假设有一个分支A,其中有Jun 1和Aug 1两个提交,以及一个分支B,其中有一个Jul 1的提交(更新以恢复下面描述的用例:分支完全独立,没有共同祖先,例如来自两个不同的仓库)。将B合并到A时,将会出现以下历史记录(根据git log):

Merged branch 'B'
Aug 1
Jul 1
Jun 1

现在,我想找到一种方法来获得相同的结果,但不需要合并提交(因此以线性历史顺序呈现,并且是重新父级提交)。 Git rebase在这里无法帮助,因为使用它,您将得到以下历史记录:

Jul 1
Aug 1
Jun 1
或者
Aug 1
Jun 1
Jul 1
换句话说,git rebase 总是将一个分支叠加在另一个分支之上,而我正在寻找一种按作者提交日期排序并插入提交的解决方案。显然,对于简单情况,可以通过手动使用 git rebase -i 进行后处理来实现所需排列顺序,但对于较大的历史记录,这不太实用,因此我正在寻找自动化的命令/脚本。用例?如果 A 和 B 分别代表同一项目的不同部分,它们恰好位于不同的仓库中,并且现在需要通过合并将它们相互结合起来,则希望以实际开发顺序展开线性历史记录是很自然的。

4
听起来你试图将线性开发路径与非线性版本控制系统结合起来。想要按时间顺序列出所有提交记录可能看起来很“自然”,但这将是错误的历史记录,因为你的团队实际上并没有在同一时间协作。关键是最终状态,即整合两个团队的努力,而不是事后不完整的合并步骤。 - Peter Bratton
2
@jordan002:问题明确指出“团队”在两个分支上进行“协作”是起始条件。至于“关键性重要性”,这个问题正是关于它的本质,而不是关于开发方法论的观点。 - pfalcon
2
@pfalcon:实际上,在你的问题中并没有提到这一点。此外,你试图解决的实际问题是什么?我们知道你想做什么,但你试图解决的问题是什么? - Infiltrator
2
我目前也面临着类似的情况,理解为什么@pfalcon想要这个功能。在我的情况下,两个团队曾经合作过,实际上,在一个存储库中提交的内容在逻辑上与另一个存储库中的提交相关并需要。因此,时间顺序确实是有意义的。 - Chris Cleeland
1
这个问题的另一个使用案例是:我有一个带有多个指向不同仓库的外部依赖的svn仓库。我想要用git重新开始一个“新分支”,其中包含所有必要的内容,但对于从现在开始的所有新版本来说更简单。将svn->git转换后,我将不得不过滤它们的目录结构,然后按时间顺序合并,以使历史记录看起来好像从未存在过外部依赖。这将导致可以编译的历史记录,至少回到最后一次更改外部依赖的时间。感谢您的答案。我会查看它们。 - Daniel Alder
显示剩余2条评论
4个回答

15

经过一番思考,我想出了如何进行如何以非交互方式运行git rebase --interactive?的方法,这也为此问题提供了完全脚本化的解决方案。

1. 将来自不同仓库的两个分支合并到一个仓库中(使用git remote add + git fetch)

2. 在其中一个分支上(根据顺序考虑首次提交),将其以非交互方式重新定位在另一个分支之上。

3. 准备以下脚本(rebase-reoder-by-date):

#!/bin/sh
awk '
/^pick/ {
            printf "%s %s ", $1, $2;
            system("echo -n `git show --format='%ai' -s " $2 "`");
            for (i = 3; i <= NF; i++) printf " %s", $i; printf "\n";
        }
' $1 | sort -k3 > $1.tmp
mv $1.tmp $1

4. 运行: GIT_SEQUENCE_EDITOR=./rebase-reoder-by-date git rebase -i <initial commit>

免责声明:所有这些操作都应该在原始仓库的副本上进行,审核/验证/测试组合分支以确保它是您期望的并包含您期望的内容,备份要随手可得。


2

[查看我的另一个答案,获得完全自动化的解决方案。我将这个作为一种导致最终解决方案的路径示例,以防有人遇到类似但不太明显的任务需要解决。]

好的,这不是对问题的真正回答(完全脚本化,自动化解决方案),而是思考和示例,说明如何可以通过(基于交互式rebase)处理来实现自动化。

首先,对于最终解决方案,git filter-branch --parent-filter 看起来就是所需的。除了我的 git-fu 不足以让我写出 1、2 或 3 行代码之外,编写独立脚本来解析所有修订版本的过程也不够酷,比 rebase -i 更费力。

因此,如果提交的作者日期可见,那么可以有效地使用 rebase -i。我的第一个想法是使用 git filter-branch --msg-filter 暂时修补提交消息,使其以作者日期开头,然后运行 rebase -i,最后取消修补消息。

但第二个想法是:何必费事呢?更好的方法是修补 rebase commit 列表,就像 rebase -i 使用的那样。所以,该过程如下:

  1. 像往常一样,将来自不同仓库的 A 和 B 分支合并到一个仓库中。
  2. 在另一个分支上进行 rebase(非交互式)。考虑应该将哪个分支重新基于哪个分支,以正确地拥有初始提交(这不能很容易地用 rebase 重写)。
  3. 开始 git rebase -i
  4. 在另一个控制台中,进入 $REPO/.git/rebase-merge/
  5. 运行:awk '/^pick/ {printf "%s %s ", $1, $2; system("echo -n git show --format='%ai' -s " $2 ""); for (i = 3; i <= NF; i++) printf " %s", $i; printf "\n"; }' git-rebase-todo > git-rebase-todo.new; mv git-rebase-todo.new git-rebase-todo
  6. 这似乎是重新排序提交的恰当位置/方式: sort -k3 git-rebase-todo >git-rebase-todo.new; mv git-rebase-todo.new git-rebase-todo
  7. 切换到原始控制台并在编辑器中重新加载 git-rebase-todo 文件,然后退出编辑器。

大功告成!实际上,如果 git rebase -i 可以在“非交互式”模式下工作,这完全可以脚本化。我为此提交了一个问题:如何以非交互方式运行 git rebase --interactive?


0

实际上,如果我理解正确的话,你可以很容易地通过git-stitch-repo来实现这个。


1
有趣的工具。不幸的是,结果是不同的分支,而不是一个。这个工具的结果是这个问题的起点。 - Daniel Alder

0

把不同的开发放在不同的行里直到它们被合并有什么问题吗?如果它们是分开的,那它们就是分开的。

有很多方法可以以时间顺序查看历史记录,而无需像你尝试的那样入侵历史。你试过git log --pretty --date-order吗?


1
如果问题中的通用描述不足够,请看以下更具体的例子:项目的客户端和服务器部分最初作为两个独立的git存储库创建。但是它们的开发是并行进行的,例如向服务器添加功能,然后将相关代码添加到客户端等等。因此,并没有“单独的开发线路”,只有存储库被分开。稍后,显然客户端和服务器都是一个项目,并且它们被视为这样处理,现在要做的就是将它们合并成一个存储库,代表它们的共同开发线路。 - pfalcon
你可以将上面的“服务器”和“客户端”替换为“主应用程序”和“库”,或者替换为“语言A中的实现”和“语言B中的实现”,或者替换为“接口”和“实现”。显然,这种用例更多或少是通用的,这就是我制定问题的方式,希望找到社区可重用的解决方案,而不仅仅是解决我的瞬间瘙痒。是的,这更像是一种智力挑战(“git可以做很多事情,它能做到这一点吗”)。所以,是的,我想找到一个解决方案,使存储库看起来像从一开始就正确地进行了开发,而不仅仅是一个解决方法。 - pfalcon
1
仅供参考,我发现这个SO问题是因为我正在尝试合并两个从Subversion克隆的Git存储库。Subversion/Git转换过程在选择单个子目录方面并不是很好,因此我们创建了单独的Git存储库。 - Huw Walters

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