注意:所讨论的分支受到不允许快速转发合并策略的影响。
一个简单的“是祖先”测试显然不够,因为沿着第二个或更晚的父链的提交也是祖先:
...o--o--A--o--o--o--T
\ /
...-o--*--B----o
\
C
在这个场景中,A
和B
都是T
的祖先,但你想接受A
而拒绝B
和C
。(假设--first-parent
是最高行。)
然而,使用git merge-base
会实际执行部分工作。虽然你不需要git merge-base
的--is-ancestor
模式,但确实需要进行一些额外的处理。
请注意,无论T
和某个祖先之间的路径如何,合并基础(例如A
或B
)始终是该祖先本身,或该祖先的某个祖先,例如如果我们将T
和C
作为一对,则为提交*
。(即使存在多个合并基础,这也成立,尽管我留给你构建证明。)
如果测试提交和分支末端的合并基础,或任意选择的所有合并基础集之一,还没有是测试提交本身,那么我们就像C
一样拒绝它。 (或者,我们可以使用--is-ancestor
来拒绝它,或者...请参见下文。)如果没有,我们必须枚举介于问题提交和分支末端之间的祖先路径。对于A
而言:
o--o--*--T
而对于B而言,它是:
*--T
/
o
*
的提交,我们需要确保第一个父提交包含了沿着这条路径列出的提交之一。最棘手的情况是那些拓扑结构类似于: o--o
/ \
...--A o--T
\ /
o--o
由于在这些提交之间的--ancestry-path
包含合并和到达A
的两种方式,其中一种是第一父路径,另一种不是。 (如果T
本身也是合并,则这是正确的。)
实际上,我们根本不需要首先找到合并基。 我们只是为了检查祖先路径而使用合并基。 如果合并基不是测试提交本身,则测试提交不是提示提交的祖先,testcommit..tipcommit
也不会包括testcommit
本身。 此外,添加--ancestry-path
——这会丢弃此处不是左侧中子对象的所有提交——然后将丢弃git rev-list
输出中的所有提交:例如C
没有是T
的祖先的后代(如果有,则C
将是一个合并基)。
因此,我们希望检查git rev-list --ancestry-path testcommit..branchtip
中的提交。 如果此列表为空,则测试提交本身不是分支尖端的祖先。 我们有一个像提交C
的情况;所以我们已经得到了答案。 如果列表非空,则将其缩小为其合并组件(再次运行--merges
,或将列表提供给git rev-list --stdin --merges
,以产生缩小的列表)。 如果此列表非空,请检查每个合并项,找到其--first-parent
ID,并确保结果在第一个列表中。
以下是实际的(尽管未经测试的)shell脚本代码:
TF=$(mktemp) || exit 1
trap "rm -f $TF" 0 1 2 3 15
git rev-list --ancestry-path $testcommit..$branch > $TF
test -s $TF || exit 1 # not ancestor
git rev-list --stdin --merges < $TF | while read hash; do
parent1=$(git rev-parse ${hash}^1)
grep "$parent1" $TF >/dev/null || exit 1 # on wrong path
done
exit 0 # on correct path
上述测试尽可能少地测试了提交,但从某种意义上来说,更加实际的方法是运行以下命令:
git rev-list --first-parent ${testcommit}^@..$branch
$testcommit
本身,则$testcommit
仅通过第一个父提交从$branch
可达。(我们使用^@
来排除$testcommit
的所有父提交,以便即使对于根提交也可以正常工作;对于其他提交,${testcommit}^
就足够了,因为我们使用了--first-parent
)。此外,如果我们确保按拓扑顺序完成,从git rev-list
命令发出的最后一个提交ID将仅在$testcommit
从$branch
可达时是$testcommit
本身。因此:hash=$(git rev-parse "$testcommit") || exit 1
t=$(git rev-list --first-parent --topo-order $branch --not ${hash}^@ | tail -1)
test $hash = "$t"
这应该就可以解决问题了。在$t
周围加上引号是为了防止其扩展为空字符串。
使用保留提交历史的no-fast-forward策略,可以在git log --first-parent
中进行grep。 如果只需要哈希码,可以改用git rev-list
。
git rev-list --first-parent | grep <commit hash>
git log
和--format
来显示所需的数据。git rev-parse HEAD~"$( git rev-list --count --first-parent --ancestry-path <commit>..HEAD )"
<commit>
,那么它是第一个父级祖先。rev-list --count --ancestry-path
测量两个提交之间的最短路径,然后获取在第一个父级链中处于该位置的提交。显然,如果检查的提交是第一个父级祖先,则这些必须相同。被压制的错误(例如第一个父级链太短)是无关紧要的。#!/bin/sh
ref="$1"
head="$2"
if [ -z "$head" ]; then
head="HEAD"
fi
commit=$( git rev-parse "$ref"^{commit} )
distance="$( git rev-list --count --ancestry-path --first-parent "$commit".."$head" )"
found="$( git rev-parse HEAD~"$distance" )"
if [ "$commit" != "$found" ]; then
echo "${ref} is not a first-parent ancestor of ${head}"
exit 1
fi
echo "${ref} is a first-parent ancestor of ${head} at a distance of ${distance}"
exit 0
git config --global alias.fp '!<script-path>'
fp
替换为您更喜欢的任何内容。将<script-path>
替换为您的脚本文件位置,但保留!
字符,这是使用外部文件所必需的。$ git fp 66e339c
66e339c is a first-parent ancestor of HEAD at a distance of 45
git rev-list --first-parent $hash --not "$hash^@" $branch
(也就是说,如果它产生了结果,那么它不在链上)。但没有。我正在阅读关于--topo-order的内容,但我不知道它如何真正帮助这里。在我看来,最终元素将始终是感兴趣的哈希(或不是)。我真的希望有人知道一种聪明而快速易懂的方法来做到这一点。但是,唉。 - Steve Benz--first-parent $hash ^$hash^@ ^$branch
不能正常工作,但我自己也见过这种情况。(正如你所说,从逻辑上讲,它应该只在$hash
可以通过--first-parent
弧从$branch
到达时才发出空值,否则发出$hash
。) - torek${hash}^@..$branch
的语法。阅读git-rev-parse
手册后,我得出结论这不是一个合法的修订范围。最终我使用了$branch --not ${hash}^@
并编辑了你的答案,希望这样可以。 - PiQuer--first-parent --ancestry-path
是有缺陷的,因为它没有将祖先路径限制在第一个父级上,而只是该路径上的步骤(这排除了基础)。因此,基础提交可以是路径上最后一个第一个父级提交的第二个父级。但它是核心命令,我不认为它很可能被修复,因为那可能会破坏工作脚本。( set -- `git rev-list --first-parent --ancestry-path --parents $base..$tip | tail -1`; [[ $2 = `git rev-parse $base` ]] )
- jthill--ancestry-path --first-parent
应该以那种方式限制遍历。 - torek