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复制提交D
和E
(仅限这两个),并将它们放在提交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
只需要一个名称或提交哈希值。然后:
- 使用提交哈希值作为限定符列出一些要复制的提交;
- 对提交哈希值执行等效于
git switch --detach
的操作;
- 复制步骤1中列出的提交;
- 将在步骤2之前所在的分支名移动到由步骤3复制的最后一个提交;并且
- 执行等效于在步骤4中移动的分支名上的
git switch
操作。
当不使用--onto
时,步骤1和2中的提交哈希值是相同的。当使用--onto
时,步骤1和2中的提交哈希值可能不同。因此,使用--onto
我们可以告诉Git:只复制一些提交,而不是许多提交。
具体来说,在没有--onto
的情况下,我们将复制所有从HEAD
可达但从(单个)参数不可达的提交,并且副本将转到(单个)参数。有了--onto
,我们可以说:将从HEAD
可达但不从我的指定限制器可达的提交复制到由我单独的--onto
参数指定的位置。在这种情况下,我们可以说不要尝试复制B
和C
。
另一方面,你也可以简单地运行:
git switch master
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
);
- 比较合并基础与当前提交(因为
HEAD
是master
,所以是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
压缩合并,则可能首先要重置
B
和
C
提交。
“如果历史记录更加复杂,分支点
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中的步骤进行比较):
- 列出从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。
- 进入到master上的分离HEAD状态,即提交BCFG。
- 复制步骤1中列出的提交,即D和E。
- 将分支名称(feature-2)移动到当前位置(提交E')。
- 再次执行类似于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
)之后发生了什么。尽管如此:
...是否有办法仅查看未被压缩合并到master
的feature-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 cherry
或git log --cherry-pick
相同的计算来从复制中消除任何patch-id与上游提交集匹配的提交。(不涉及A...B
对称差分符号工作原理的详细信息,很难定义此集合。)在您的情况下,这也无关紧要,因为这种补丁ID匹配在这里极不可能发生。它更适用于这样一种情况:上游故意使用git cherry-pick
将您的一个或多个提交复制到您要进行rebase的分支上。
- 在某些情况下,但再次不包括您的情况,
git rebase
默认运行git merge --fork-point
以查找要省略的提交,这可能会产生出人意料的结果。
重新基础文档历史上一直没有提到这些问题,可能是因为它们并不经常发生。在您的情况下,它们不应该出现。最新的重新基础文档改进了很多。