这可能有助于重新绘制这个图形:
feature/X C--E--F
/ \
master -A--B--D---G--H--I--J->
as this:
C--E--F <-- feature/X
/ \
<--A--B--D---G--H--I--J <-- master
原因在于箭头确实指向后面,
feature/X
指向分支
feature/X
的尖端提交
F
,而
master
指向
master
分支的尖端提交(我假设这里是
J
,但也许根据您的原始图可能有更多)。
正如您所指出的,
feature/X ^master
(也可以拼写为
master..feature/X
)失败是因为从
master
开始,通过向后工作,可以到达提交
F
。当我们到达提交
H
时,同时向后遍历两个父提交,因此请求消除所有可从
master
到达的提交也消除了
C--E--F
序列。
为了防止这种情况发生,我们必须从某个点开始消除提交,即在将
feature/X
的末端合并到
master
之前的第一次合并之前的点。任何一个提交
G
、
D
或
B
都可以。也就是说,如果我们有
任何一个这些提交的哈希值,那么:
git rev-list feature/X ^$hash
qzb's method 找到提交 D
,然后使用后缀 ^
来标识它的第一个且唯一的父提交。它的工作原理是列出从 J
(master
的末尾)可达但从 F
(feature/X
的末尾)不可达的每个提交。但需要注意的是:git rev-list
可能会对提交进行排序,因此 D
实际上可能并不是最后一个被列出的提交,但是 | tail -1
假设列表以提交 D
的哈希值结尾。
因此,这取决于提交中存储的时间戳。如果它们按顺序进行(因此,随着提交向前移动,日期都会增加),那么这就不是问题。通常情况下确实如此。但有时您可能会按“错误”的日期顺序添加提交,这是由于时钟设置不正确或在不同计算机上执行提交时存在时间差异等原因造成的。
我们可以通过告诉
git rev-list
使用
--topo-order
来修复日期假设,这将强制它按照图形顺序(使用图形拓扑的偏序)列出提交。因此,在使用此方法时,请添加
--topo-order
。
Noufal Ibrahim的方法是通过使用
git log
查找提交
H
来实现的。最好使用
git rev-list
,它与
git log
使用相同的选项,但只打印哈希值(这就是我们想要的)。
H=$(git rev-list --merges -1 master)
# H stands for Hash, and also for "commit H" :-)
(请注意,我们必须为图形遍历指定起点,而
git log
默认从
HEAD
开始)。仅获取提交
H
的哈希值是不够的,因为我们必须向后爬一个父级。由于
H
有
两个父级,我们必须小心地从
H
爬到
G
(而不是
F
)。
幸运的是,每当我们使用
git merge
合并时,Git都会确保新合并提交的
第一个父级是当前分支上的提交。也就是说,当我们通过运行
git merge feature/X
进行提交
H
时,我们在分支
master
上,名称
master
表示提交
G
。因此,
H
的
第一个父级是
G
,因此
$H^1
或
$H^
仅标识提交
G
。
H=$(git rev-list --merges -1 master)
git rev-list feature/X ^${H}^
花括号 H
周围的内容在技术上并不是必需的,只是为了清晰起见:我们展开 $H
,然后在展开后面放置 ^
(以标识提交 G
),再在展开前面放置另一个 ^
(告诉 git rev-list
我们将其用作排除限定符)。
由于 $yes ^$no
可以写成 $no..$yes
,因此我们也可以这样写:
H=$(git rev-list --merges -1 master)
git rev-list ^${H}^..feature/X
这种方法更有效率(我们只枚举了一个提交
H
,而不是使用
tail -1
获取可能很长的链的最后一个提交),并且不会受到日期顺序问题的影响(但我们在上面看到如何使用
--topo-order
来解决这些问题)。
顺便提一下,在找到提交
H
时,也应该像前面一样使用
--topo-order
,原因相同:我们不希望 Git 对某些其他合并(例如在
A
之前)进行排序并放在
H
前面。
剩余的缺陷
qzb 指出其中一个:虽然
feature/X
指向提交
F
,但如果过去有更多合并,则我们不一定“知道何时停止”。也就是说:
o--o---o--o--o <-- feature
/ \ / \
...--o--o--o---o--o--o---o <-- master
通过以这种特定的方式绘制此特定图表,我们清楚地看到“顶部行”上的所有提交都是在
feature
上完成的,而
feature
被合并了两次,而
master
被合并回了一次
feature
。(顺便说一句,这种“交叉合并”可能会让你陷入麻烦。它不是
错误的,但通常情况下,您应该小心将A合并到B中,然后将B合并回A。在某些情况下,这会产生多个合并基础,这可能会很棘手。)但对于Git来说并不清晰,并且有其他的绘制图表的方式也会模糊我们的视线。(此外,如果您允许“快进”合并(而不是非快进的实际合并提交),那么解开分支历史就变得不可能了。再次强调,这不是
错误的,您只需要准备好处理它。)
如果在master
分支的提交H
之后进行合并,那么这两种方法都会出现一个更重要的问题。也就是说,假设我们到目前为止所画的字母图还有一点误导性,并且实际上应该像这样:
C--E--F <-- feature/X
/ \
<--A--B--D---G--H--I--J <-- master
/
<-o--o--o <-- feature/Y
Now if we do:
H=$(git rev-list --topo-order --merges -1 master)
我们最终将把
$H
指向提交
I
,而不是提交
H
。原因很简单:我们要求从
master
开始向后工作,找到最近(拓扑上)的合并提交。这就是提交
I
。但
I^
是提交
H
,排除
H
将使随后的
git rev-list
排除提交
C-E-F
。
这似乎注定了这种方法要失败;我们能否回到查找提交
D
的位置?不行,因为qzb的技巧:
$(git rev-list feature/X..master | tail -n 1)
当Git沿着I
的第二个父节点,即feature/Y
,并开始列出所有这些提交时停止工作。如果没有使用--topo-order
,我们会得到最旧的提交。使用--topo-order
仍然无法告诉我们哪个链(I^1
vs I^2
)首先处理。如果该链在提交A
或更早的提交处连接回来,则可能得到提交A
-或-更早提交的哈希值,而不是提交D
的哈希值。
我们可以通过注意到带来
feature/Y
的附加合并
I
,并排除
feature/Y
,以便Git不会沿着该链路竞争来解决这个问题。但这开始变得复杂。那么我们真正需要的不是
最近的合并,而是
带入提交F
的合并(即,“找到提交
H
”)。有没有办法做到这一点呢?事实证明确实有。我们想要的是
--ancestry-path
。
--ancestry-path
选项剥离掉不是被排除提交的后代的提交。由于feature/X
合并到了master
中,我们可以确定,在F
之后,有些提交(当然是H
)是F
的后代——即,F
是其中一个父提交——也是master
的祖先。所以:
git rev-list --ancestry-path --topo-order ^feature/X master
该命令告诉Git列出提交
H
、
I
和
J
,
没有其他提交。也就是说,我们不会沿着合并
I
带来的其他提交飞奔而去:这些提交将被修剪。
如果我们然后丢弃除最后一次提交以外的所有提交(再次使用
tail -1
),并选择使用
--merges
来在使用
tail
之前丢弃任何非合并提交,那么即使
I
或
J
是一个合并提交,这将让我们定位到提交
H
。
H=$(git rev-list --ancestry-path --topo-order \
--merges ^feature/X master | tail -1)
git rev-list ^$H^..feature/X
这是两种方法的混合体:我们使用
--ancestry-path
从
H
开始查找提交,使用
tail -1
去掉除提交
H
以外的所有提交,然后使用
^$H^
排除提交
G
及之前的提交。
git rev-list sha1_of_B..feature/X
是什么意思? - rudimeier