从压扁的树枝上分出

clw*_*ght 5 git pull-request git-squash

假设我有以下git的历史:一个主分支开始犯A,一个feature-1分支分支的关闭A与提交BC,第二特性分支feature-2所建立起来的承诺关闭C与提交DE

master     A
            \
feature-1    B--C
                 \
feature-2         D--E
Run Code Online (Sandbox Code Playgroud)

现在假设提交C已经过测试并准备好合并,所以我们使用git switch master; git merge feature-1 --squash.

master     A------C'
            \    /
feature-1    B--C
                 \
feature-2         D--E
Run Code Online (Sandbox Code Playgroud)

master 的历史记录很好而且干净,只有提交AC',但是如果我们现在想要比较masterfeature-2(例如,git log master..feature-2),我们最终会看到所有提交feature-1都已经合并了。

问题 1:是否有一种简单的方法可以feature-2压缩历史以匹配压缩的合并?如果什么历史是一个有点复杂,它的分支点后有更多的提交Cfeature-1那名壁球合并成高手?

问题 2:假设重写历史很困难(或者只能用 很乏味地完成git rebase -i;我在每个分支上有两个以上的提交),有没有办法只查看feature-2未合并的提交成为大师?在 GitHub 或 Bitbucket 上为 执行拉取请求时feature-2 -> master,有没有办法只列出那些真正新的提交?

tor*_*rek 8

现在假设提交C已经过测试并准备好合并,所以我们使用git switch master; git merge feature-1 --squash.

master     A------C'
            \    /
feature-1    B--C
                 \
feature-2         D--E
Run Code Online (Sandbox Code Playgroud)

这幅画不太对劲:它应该像我在下面画的那样。请注意,我也将名称移到了右侧,原因稍后会变得更清楚。我也称壁球提交BC,这是为了明确表明有一个提交可以完成 B 和 C 一起做的事情。

你画的是一个真正的合并(虽然你称之为合并提交C')。正如马特所说,“壁球合并”根本不是合并。

master     A------C'
            \    /
feature-1    B--C
                 \
feature-2         D--E
Run Code Online (Sandbox Code Playgroud)

在这一点上,几乎没有理由保留 name feature-1。如果删除它,我们可以像这样重新绘制图形:

A--BC   <-- master
 \
  B--C   <-- feature-1
      \
       D--E   <-- feature-2
Run Code Online (Sandbox Code Playgroud)

请注意,提交A-B-C-D-E都在分支上feature-2(无论我们是否删除了 name feature-1);提交BC仅在master.

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

问题 1:是否有一种简单的方法可以feature-2压缩历史以匹配压缩的合并?

我并不完全清楚你所说的“压制历史”是什么意思。git merge --squash但是,运行上述命令后, commit 中的快照BC将(完全匹配) commit 中的快照C,因此运行:

git switch feature-2 && git rebase --onto master feature-1
Run Code Online (Sandbox Code Playgroud)

(注意--onto这里的1)将告诉 Git 复制提交D并且E(仅)在 commit 之后复制BC,如下所示:

A--BC   <-- master
 \
  B--C--D--E   <-- feature-2
Run Code Online (Sandbox Code Playgroud)

现在可以安全地删除名称,feature-1因为我们不再需要记住 commit 的哈希 ID C。如果我们停止绘制废弃的提交,我们最终会得到:

git switch feature-2 && git rebase --onto master feature-1
Run Code Online (Sandbox Code Playgroud)

这可能是你想要的。


1通常,git rebase采用一个名称或提交哈希 ID。那么它:

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

使用--onto时,提交在步骤1散列ID和2是相同的。使用 时--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
Run Code Online (Sandbox Code Playgroud)

如果您只想要 DE 链的单个压缩合并:

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

git merge --squash通常顺利进行为好,因为,像普通的git mergegit merge --squash由开始:

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

第一个 diff 显示B+C做了什么,因为BCs 的快照匹配Cs,第二个显示B+C+D+E做了什么,因为E的快照是Bplus Cplus Dplus的结果E。所以除非D和/或E特别做某事B和/或C做了这两组更改很可能会自动合并。

(请注意,rebase 在这里总是顺利进行,即使D和/或E撤消某些操作。)

squash-not-really-a-merge 和真正的合并之间的区别仅限于最终的提交:在这种情况下BC,squash 有一个单一父级的提交,而真正的合并有两个父级的提交。在这种情况下,真正的合并会让你BC成为一个父母,E另一个。如果您喜欢进行壁球合并,您可能希望首先重新设置BC提交BC

如果历史记录稍微复杂一点,并且C在功能 1 上的分支点之后有更多提交被压缩合并到 master 会怎样?

与往常一样,诀窍是绘制一个实际的图形。我们可以从这个开始:

A--BC   <-- master
     \
      D'-E'  <-- feature-2
Run Code Online (Sandbox Code Playgroud)

之后git switch master && git merge --squash feature-1,产生:

git switch master             # if needed - you're probably already there
git merge --squash feature-2
Run Code Online (Sandbox Code Playgroud)

现在适合使用:

git switch feature-2 && git rebase --onto master feature-1
Run Code Online (Sandbox Code Playgroud)

请注意,这与我们在早期情况中使用的命令相同。 它说(与上面脚注 1 中的步骤相比):

  1. 列出可从feature-2(我们在 之后的位置git switch但不能从feature-1访问的提交。可访问的提交feature-2A-B-C-D-E,可访问的提交feature-1A-B-C-F-GA-B-C-F-GA-B-C-D-E叶子中减去D-E

  2. 进入一个分离的 HEADmaster,即 commit BCFG

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

  4. 将分支名称( feature-2)拉到我们现在所在的位置(commit E')。

  5. 再做相当于git switch feature-2

结果是:

A--BC--DE   <-- master (HEAD)
 \
  B--C   <-- feature-1
      \
       D--E   <-- feature-2
Run Code Online (Sandbox Code Playgroud)

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

问题 2:假设重写历史很困难(或者只能用一个git rebase -i; 我在每个分支上有超过两次提交)......

正如您在上面看到的,这不一定是正确的假设。rebase 的难度取决于每次要复制的提交有多少合并冲突,这取决于最后一次常见提交之后发生的事情(C在上图中)。仍然:

...有什么方法可以只查看feature-2没有合并到的提交master

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

git log feature-1..feature-2
Run Code Online (Sandbox Code Playgroud)

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

在 github 或 bitbucket for 上执行拉取请求时feature-2 -> master,有没有办法只列出那些真正新的提交?

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


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

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

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