从压扁的树枝上分枝
Branching off of squashed branches
假设我有以下 git 历史记录:一个以提交 A
开始的主分支,一个从 A
分支出来的 feature-1
分支,提交 B
和 C
,以及第二个功能分支 feature-2
,它基于提交 C
和提交 D
和 E
.
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 的历史非常干净,只有提交 A
和 C'
,但是如果我们现在想要比较 master
和 feature-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
,这使得复制提交 D
和 E
(而不是其他提交)变得容易) 到新的和改进的提交 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
(注意这里的 --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
取 一个 名称或提交哈希 ID。然后:
- 列出一组要复制的提交,使用提交哈希 ID 作为限制器;
- 在提交哈希 ID 上相当于
git switch --detach
;
- 复制步骤 1 中列出的提交;
- 移动您在第 2 步之前所在的分支名称,指向第 3 步复制的最后一次提交;和
- 相当于
git switch
回到步骤 4 中刚刚移动的分支名称。
当不使用--onto
时,步骤1和2中的提交哈希ID相同。使用 --onto
时,步骤 1 和 2 中的提交哈希 ID 是,或者至少可以是, 不同的 。所以使用 --onto
我们可以告诉 Git:只复制 一些 提交,而不是许多提交。
具体来说,如果没有 --onto
,我们将复制所有 可从 HEAD
到达的提交,但 不会 可从(单个)参数访问,副本将转到(单个)参数。使用 --onto
,我们可以说:复制可从 HEAD
而不是从我指定的限制器到达的提交,到我单独的 --onto
参数指定的位置。 在这种情况下,让我们说 不要尝试复制 B
和 C
.
另一方面,您也可以简单地 运行:
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
,因为 HEAD
是 master
,它标识提交 BC
);和
- 根据指定的提交区分合并基础(
E
,因为 feature-2
命名提交 E
)。
第一个 diff 显示了 B+C
做了什么,因为 BC
的快照匹配 C
s,第二个显示了 B+C+D+E
做了什么,因为 E
的快照是B
加C
加D
加E
的结果。所以除非 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.
,您可能希望先将 B
和 C
提交变基
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 中的步骤比较):
列出可从 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
.
在 master
进入分离的 HEAD,即提交 BCFG
.
复制步骤 1 中列出的提交,即 D
和 E
.
复制分支名称 (feature-2
) 绕到我们现在所在的位置 (commit 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
的简单方法。
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 cherry
或 git log --cherry-pick
相同的计算来从复制中消除任何提交 patch-id 匹配上游提交集中的一个提交。 (如果不深入了解 A...B
对称差分符号的工作原理,就很难定义这个集合。)在你的情况下也没有关系,因为这种 patch-ID 匹配在这里是极不可能的.对于上游某人故意使用 git cherry-pick
将您的一个或多个提交复制到您要变基的分支的情况,这意味着更多。
- 在某些情况下——但同样不是你的——
git rebase
默认为 运行ning git merge --fork-point
来查找要省略的提交,这会产生令人惊讶的结果。
rebase 文档在历史上一直没有提到这些,可能是因为它们不经常出现。在你的情况下,他们不应该出现。最新的rebase文档有了很大的改进。
假设我有以下 git 历史记录:一个以提交 A
开始的主分支,一个从 A
分支出来的 feature-1
分支,提交 B
和 C
,以及第二个功能分支 feature-2
,它基于提交 C
和提交 D
和 E
.
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 的历史非常干净,只有提交 A
和 C'
,但是如果我们现在想要比较 master
和 feature-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 usegit 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
,这使得复制提交 D
和 E
(而不是其他提交)变得容易) 到新的和改进的提交 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
(注意这里的 --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
取 一个 名称或提交哈希 ID。然后:
- 列出一组要复制的提交,使用提交哈希 ID 作为限制器;
- 在提交哈希 ID 上相当于
git switch --detach
; - 复制步骤 1 中列出的提交;
- 移动您在第 2 步之前所在的分支名称,指向第 3 步复制的最后一次提交;和
- 相当于
git switch
回到步骤 4 中刚刚移动的分支名称。
当不使用--onto
时,步骤1和2中的提交哈希ID相同。使用 --onto
时,步骤 1 和 2 中的提交哈希 ID 是,或者至少可以是, 不同的 。所以使用 --onto
我们可以告诉 Git:只复制 一些 提交,而不是许多提交。
具体来说,如果没有 --onto
,我们将复制所有 可从 HEAD
到达的提交,但 不会 可从(单个)参数访问,副本将转到(单个)参数。使用 --onto
,我们可以说:复制可从 HEAD
而不是从我指定的限制器到达的提交,到我单独的 --onto
参数指定的位置。 在这种情况下,让我们说 不要尝试复制 B
和 C
.
另一方面,您也可以简单地 运行:
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
,因为HEAD
是master
,它标识提交BC
);和 - 根据指定的提交区分合并基础(
E
,因为feature-2
命名提交E
)。
第一个 diff 显示了 B+C
做了什么,因为 BC
的快照匹配 C
s,第二个显示了 B+C+D+E
做了什么,因为 E
的快照是B
加C
加D
加E
的结果。所以除非 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.
B
和 C
提交变基
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 中的步骤比较):
列出可从
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
.在
master
进入分离的 HEAD,即提交BCFG
.复制步骤 1 中列出的提交,即
D
和E
.复制分支名称 (
feature-2
) 绕到我们现在所在的位置 (commitE'
).再做一次相当于
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 intomaster
?
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 cherry
或git log --cherry-pick
相同的计算来从复制中消除任何提交 patch-id 匹配上游提交集中的一个提交。 (如果不深入了解A...B
对称差分符号的工作原理,就很难定义这个集合。)在你的情况下也没有关系,因为这种 patch-ID 匹配在这里是极不可能的.对于上游某人故意使用git cherry-pick
将您的一个或多个提交复制到您要变基的分支的情况,这意味着更多。 - 在某些情况下——但同样不是你的——
git rebase
默认为 运行ninggit merge --fork-point
来查找要省略的提交,这会产生令人惊讶的结果。
rebase 文档在历史上一直没有提到这些,可能是因为它们不经常出现。在你的情况下,他们不应该出现。最新的rebase文档有了很大的改进。