从压缩的分支中分离出分支

14
假设我有以下 Git 历史记录:一个以提交 A 开始的主分支,一个从 A 分支出来的 feature-1 分支,其中包含提交 BC,以及第二个功能分支 feature-2,它是从提交 C 开始构建的,其中包含提交 DE
master     A
            \
feature-1    B--C
                 \
feature-2         D--E

现在假设提交 C 已经经过测试,并准备合并,那么我们使用 git switch master; git merge feature-1 --squash 命令。

master     A------C'
            \    /
feature-1    B--C
                 \
feature-2         D--E

对于master分支的历史记录非常干净,只有AC'两个提交,但是如果我们现在想比较masterfeature-2(例如:git log master..feature-2),我们会看到来自feature-1的所有已经合并的提交。

问题1:有没有一种简单的方法来压缩feature-2的历史记录以匹配压缩的合并记录?如果历史记录更加复杂,在feature-1上的分支点C之后还有更多的提交被压缩合并到了master上,那该怎么办呢?

问题2:假设重写历史很困难(或者只能通过繁琐的git rebase -i来完成;我的每个分支都有很多提交),有没有什么方法可以仅查看feature-2中未被压缩合并到master中的提交?当在GitHub或Bitbucket上进行拉取请求时,有没有仅列出那些真正新的提交的方法?


只需将主分支合并到feature-2分支即可。两个特性分支在主分支中共享一个共同的祖先。只需将feature-2分支压缩合并到主分支中,就可以了。不需要过于复杂。 - Greg Burghardt
3个回答

23

Now suppose that commit C has been tested and is ready to merge in, so we use git switch master; git merge feature-1 --squash.

master     A------C'
            \    /
feature-1    B--C
                 \
feature-2         D--E

这张图不太对:应该按照我下面画的方式来。注意,我也把名称移到右边了,原因一会儿就会变得更清楚。我还将 squash commit 命名为 BC,这是为了明确表明有一个单一的提交执行了 B 和 C 两个操作。

你画的是一个真正的合并(虽然你称合并提交为 C')。正如 matt 说的那样,“压缩合并”根本不是合并。

A--BC   <-- master
 \
  B--C   <-- feature-1
      \
       D--E   <-- feature-2

目前来看,保留名称为feature-1的理由已经几乎没有了。如果您将其删除,我们可以按照以下方式重新绘制图表:

A--BC   <-- master
 \
  B--C--D--E   <-- feature-2

请注意,提交记录 A-B-C-D-E 都在分支 feature-2 上(无论我们是否删除 feature-1 名称);提交记录 BC 仅在主分支上。
保留 feature-1 名称的主要原因是它可以标识提交记录 C,这使得很容易将提交记录 D 和 E(以及没有其他提交记录)复制到新的和改进的提交记录 D'-E'。
问题1:有没有一种简单的方法来压缩 feature-2 的历史记录以匹配压缩合并?
我不完全清楚你所说的“压缩历史记录”的意思。虽然运行了上面的 git merge --squash,但提交记录 BC 中的快照将与提交记录 C 中的快照完全匹配,因此运行:
git switch feature-2 && git rebase --onto master feature-1

(注意这里的--onto 1)将告诉Git复制提交DE(仅限这两个),并将它们放在提交BC之后,如下图所示:

      D'-E'  <-- feature-2 (HEAD)
     /
A--BC   <-- master
 \
  B--C   <-- feature-1
      \
       D--E   [abandoned]

现在可以安全地删除名称为feature-1的名称,因为我们不再需要记住提交C的哈希ID。如果我们停止绘制弃用的提交,则最终结果为:
A--BC   <-- master
     \
      D'-E'  <-- feature-2

这可能是您想要的。

1通常情况下,git rebase只需要一个名称或提交哈希值。然后:

  1. 使用提交哈希值作为限定符列出一些要复制的提交;
  2. 对提交哈希值执行等效于git switch --detach的操作;
  3. 复制步骤1中列出的提交;
  4. 将在步骤2之前所在的分支名移动到由步骤3复制的最后一个提交;并且
  5. 执行等效于在步骤4中移动的分支名上的git switch操作。

当不使用--onto时,步骤1和2中的提交哈希值是相同的。当使用--onto时,步骤1和2中的提交哈希值可能不同。因此,使用--onto我们可以告诉Git:只复制一些提交,而不是许多提交

具体来说,在没有--onto的情况下,我们将复制所有从HEAD可达但从(单个)参数不可达的提交,并且副本将转到(单个)参数。有了--onto,我们可以说:将从HEAD可达但不从我的指定限制器可达的提交复制到由我单独的--onto参数指定的位置。在这种情况下,我们可以说不要尝试复制BC


另一方面,你也可以简单地运行:
git switch master             # if needed - you're probably already there
git merge --squash feature-2

如果您只想要一次D-E链的单一合并:

A--BC--DE   <-- master (HEAD)
 \
  B--C   <-- feature-1
      \
       D--E   <-- feature-2

这个git merge --squash通常也会很顺利,因为像普通的git merge一样,git merge --squash开始于:
  • 找到合并基础(在本例中是A);
  • 比较合并基础与当前提交(因为HEADmaster,所以是BC),以及
  • 比较合并基础与指定的提交(因为feature-2命名了提交E)。
第一个差异显示了B+C的内容,因为BC的快照与C的相同,而第二个差异显示了B+C+D+E的内容,因为E的快照是B加上C加上D加上E的结果。因此,除非D和/或E明确反转了B和/或C的变更,否则这两组变更很可能会自动合并。
(请注意,即使D和/或E撤消了某些内容,重置也总是会顺利进行。)
压缩-实际上并不是合并和真正的合并之间的区别仅限于最终提交:压缩有一个带有单个父项的提交,在这种情况下为BC,而真正的合并有一个带有两个父项的提交。在这种情况下,真正的合并会给您BC作为一个父项,E作为另一个父项。如果您喜欢BC压缩合并,则可能首先要重置BC提交。
“如果历史记录更加复杂,分支点C后有更多提交被压缩合并到主分支中怎么办?”
像往常一样,诀窍是绘制实际图形。我们可以从这里开始:
A   <-- master
 \
  B--C--F--G   <-- feature-1
      \
       D--E   <-- feature-2

在执行git switch master && git merge --squash feature-1之后,会产生以下结果:

A--BCFG   <-- master
 \
  B--C--F--G   <-- feature-1
      \
       D--E   <-- feature-2

现在可以使用:

git switch feature-2 && git rebase --onto master feature-1

请注意,这是我们在早期情况下使用的相同命令。它表示(与上面脚注1中的步骤进行比较):
  1. 列出从feature-2可达的提交(在git switch之后的位置),但不包括feature-1。从feature-2可达的提交为A-B-C-D-E,从feature-1可达的提交为A-B-C-F-G。从A-B-C-F-G中减去A-B-C-D-E,剩下D-E。
  2. 进入到master上的分离HEAD状态,即提交BCFG。
  3. 复制步骤1中列出的提交,即D和E。
  4. 将分支名称(feature-2)移动到当前位置(提交E')。
  5. 再次执行类似于git switch feature-2的操作。
结果是:
        D'-E'  <-- feature-2 (HEAD)
       /
A--BCFG   <-- master
 \
  B--C--F--G   <-- feature-1
      \
       D--E   [abandoned]

之后可以安全地删除名称feature-1: 我们不再需要通过提交G找到提交C的简便方法。

问题2:假设重写历史很难(或只能通过git rebase -i费力地完成;我在每个分支上有超过两个提交)...

如上所示,这并不一定是正确的假设。重写基础取决于每个要复制提交产生的合并冲突数量,这取决于最后一个公共提交(上图中的C)之后发生了什么。尽管如此:

...是否有办法仅查看未被压缩合并到masterfeature-2中的提交?

git log命令具有此功能的简单语法,只要您仍然使用名称feature-1标识适当的提交,就像上面的各种图示中一样:

git log feature-1..feature-2

这个语法可以做到这一点。这个语法的意思是从feature-2开始向后追溯并减去从feature-1开始向后追溯的所有提交。请注意,这是我们在上面的示例中使用git rebase操作复制的提交集合。2

在github或bitbucket上执行feature-2 -> master的pull request时,是否有办法仅列出那些真正的新提交?

没有,因为这些系统没有相应的语法。但是,一旦您使用rebase只复制所需的提交,并强制推送使GitHub或Bitbucket存储库匹配,它们将显示您想要的内容。


2上面没有提到的是,git rebase默认情况下会有意省略第1步中的某些提交。在您的情况下,这里没有应该被省略的提交,因此这并不重要,但值得一提的是:

  • 默认情况下,git rebase会省略所有合并提交。
  • 由于设计原因(没有停止此操作的选项),git rebase还使用与git cherrygit log --cherry-pick相同的计算来从复制中消除任何patch-id与上游提交集匹配的提交。(不涉及A...B对称差分符号工作原理的详细信息,很难定义此集合。)在您的情况下,这也无关紧要,因为这种补丁ID匹配在这里极不可能发生。它更适用于这样一种情况:上游故意使用git cherry-pick将您的一个或多个提交复制到您要进行rebase的分支上。
  • 在某些情况下,但再次不包括您的情况,git rebase默认运行git merge --fork-point以查找要省略的提交,这可能会产生出人意料的结果。

重新基础文档历史上一直没有提到这些问题,可能是因为它们并不经常发生。在您的情况下,它们不应该出现。最新的重新基础文档改进了很多。


1
这非常详尽和有帮助。谢谢! - clwainwright
非常好的解释,如果 feature-1 分支已经消失了,知道如何解决这个问题会很有趣。 - Fleshgrinder
@Fleshgrinder:没有简单通用的方法,但如果你确定进行了压缩合并,你可以沿着这条线重复压缩合并提交,直到得到与目标分支中显示的相同结果。如果存在还原提交,则可能会找到“错误”的合并,因此没有简单通用的方法。还要注意,有时候,压缩合并记录了被压缩提交的主题,这将作为有价值的线索。 - torek

1
在简单的情况下,如果您只是将feature-2基于master进行rebase,那么您只会有非空提交。
在更复杂的情况下,我建议按照以下步骤操作:
git switch feature-2
git merge origin/master
git reset --soft origin/master
git commit -m 'feature 2'

这将导致一个单一的提交,其中包含了所有来自feature-2的更改,有效地压缩在master之上。

0

在将第一个 PR feature-1(父级)合并到主分支后,您会得到包含所有feature-1feature-2的第二个 PR(子级)。

有时,为了保持精神健康,我会这样做:

git switch feature-2

git log

然后按顺序复制所有我知道来自feature-2的真实提交哈希。省略任何来自feature-1的内容。

然后

git reset origin/main --hard

git cherry-pick hash-1

git cherry-pick hash-2

git cherry-pick hash-3

以此类推,就像手动重写历史一样。


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