从压扁的树枝上分枝

Branching off of squashed branches

假设我有以下 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 -> master 执行拉取请求时,有没有办法只列出那些真正新的提交?

在简单的情况下,如果您在 master 上简单地 rebase 您的 feature-2,您将只有非空提交。

在更复杂的情况下,我建议执行以下操作:

git switch feature-2
git merge origin/master
git reset --soft origin/master
git commit -m 'feature 2'

这将导致一次提交,其中包含来自 feature-2 的所有更改,有效压缩在 master

之上

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 提交 BC,这是为了表明有一个提交可以完成 B-and-C 一起完成的事情。

你画的是真正的合并(虽然你称合并提交C')。正如 matt said,“挤压合并”根本不是合并。

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 仅在 master.

保留名称 feature-1 的主要原因是它标识提交 C,这使得复制提交 DE(而不是其他提交)变得容易) 到新的和改进的提交 D'-E'.

Question 1: Is there an easy way to squash the history for feature-2 to match the squashed merge?

我不太清楚你所说的“压制历史”是什么意思。有了 运行 以上 git merge --squash,但是,提交 BC 中的快照将(完全)匹配提交 C 中的快照,所以 运行ning:

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

(注意这里的 --onto1)会告诉 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一个 名称或提交哈希 ID。然后:

  1. 列出一组要复制的提交,使用提交哈希 ID 作为限制器;
  2. 在提交哈希 ID 上相当于 git switch --detach
  3. 复制步骤 1 中列出的提交;
  4. 移动您在第 2 步之前所在的分支名称,指向第 3 步复制的最后一次提交;和
  5. 相当于 git switch 回到步骤 4 中刚刚移动的分支名称。

使用--onto时,步骤1和2中的提交哈希ID相同。使用 --onto 时,步骤 1 和 2 中的提交哈希 ID 是,或者至少可以是, 不同的 。所以使用 --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 链中的单个 squash-merge:

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

这个 git merge --squash 通常 也会顺利进行,因为,像常规 git merge 一样,git merge --squash 开始于:

  • 找到合并基础(A 在这种情况下);
  • 将合并基础与当前提交进行比较(BC,因为 HEADmaster,它标识提交 BC);和
  • 根据指定的提交区分合并基础(E,因为 feature-2 命名提交 E)。

第一个 diff 显示了 B+C 做了什么,因为 BC 的快照匹配 Cs,第二个显示了 B+C+D+E 做了什么,因为 E的快照是BCDE的结果。所以除非 D and/or E 特别地 un-做某事 B and/or C 做了,两组更改很可能会自动合并。

(请注意,这里的变基总是很顺利,即使 D and/or E 撤消了某些操作。)

squash-not-really-a-merge 和真正的合并之间的区别仅限于最终提交:squash 有一个带有单个 parent 的提交,在本例中为 BC,而真正的合并有两个 parents 的提交。在这种情况下,真正的合并会给你 BC 作为一个 parent,而 E 作为另一个。如果您喜欢 BC squash-merge.

,您可能希望先将 BC 提交变基

What if the history is a little more complicated and there were more commits after the branch point C on feature-1 that were squash-merged into master?

一如既往,诀窍是绘制实际图形。我们可以从这个开始:

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-D-E 中减去 A-B-C-F-G 得到 D-E.

  2. master 进入分离的 HEAD,即提交 BCFG.

  3. 复制步骤 1 中列出的提交,即 DE.

  4. 复制分支名称 (feature-2) 绕到我们现在所在的位置 (commit 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 的简单方法。

Question 2: Assuming that rewriting history is hard (or can only be tediously done with a git rebase -i; I've got way more than two commits on each branch) ...

正如您在上面看到的,这不一定是正确的假设。 rebase 的难易程度取决于每次 to-be-copied 提交时发生的合并冲突的数量,这取决于最后一次共同提交(上图中的 C)之后发生的情况。仍然:

... is there any way to view only the commits in feature-2 that weren't squash-merged into master?

git log 命令对此有一个简单的语法,只要您仍然有名称 feature-1 来标识适当的提交,如上图所示:

git log feature-1..feature-2

就是这样做的。此语法意味着 feature-2 开始并向后工作可到达的所有提交,减去从 feature-1 开始并向后工作可到达的所有提交。 请注意,这是相同的我们在上面的示例中使用 git rebase 操作复制的一组提交。2

When performing a pull request on github or bitbucket for feature-2 -> master, is there any way to only list those genuinely new commits?

不,因为这些系统没有等效的语法。但是,一旦您使用 rebase 仅复制所需的提交,并使用 force-push 使 GitHub 或 Bitbucket 存储库匹配,它们就会显示您想要的内容。


2上面没有提到的是git rebase在默认情况下故意省略了步骤1中的某些提交。在你的情况下,这里没有应该省略的提交,所以这不是很相关,但值得一提:

  • 默认情况下,git rebase 忽略所有 merge 提交。
  • 通过设计(并且没有选项可以阻止它),git rebase 也使用与 git cherrygit log --cherry-pick 相同的计算来从复制中消除任何提交 patch-id 匹配上游提交集中的一个提交。 (如果不深入了解 A...B 对称差分符号的工作原理,就很难定义这个集合。)在你的情况下也没有关系,因为这种 patch-ID 匹配在这里是极不可能的.对于上游某人故意使用 git cherry-pick 将您的一个或多个提交复制到您要变基的分支的情况,这意味着更多。
  • 在某些情况下——但同样不是你的——git rebase 默认为 运行ning git merge --fork-point 来查找要省略的提交,这会产生令人惊讶的结果。

rebase 文档在历史上一直没有提到这些,可能是因为它们不经常出现。在你的情况下,他们不应该出现。最新的rebase文档有了很大的改进。