5

我正在尝试编写一种方法来判断给定提交是否在给定分支的第一个父链上。因此,例如,merge-base 不会飞,因为提交可能已被合并。我想知道确切的提交是否曾经是分支的尖端。

注意:有问题的分支受制于非快进合并策略。

4

3 回答 3

2

no-fast-forward 策略意味着您可能可以在git log --first-parent. 您可能只需要哈希,因此您可以git rev-list使用

git rev-list --first-parent | grep <commit hash>

否则使用--formatwithgit log来显示你想要的数据。

编辑:这篇文章可以给你一些想法

如何判断一个提交是否是另一个提交的祖先(反之亦然)?

于 2017-03-28T20:55:51.480 回答
2

花哨的方式

一个简单的“是祖先”测试显然是行不通的,因为提交到第二个或更晚的父链也是祖先:

...o--o--A--o--o--o--T
    \            /
 ...-o--*--B----o
         \
          C

A和都是B的祖先T,但你想接受A而拒绝BC。(假设--first-parent是这里的第一行。)

git merge-base然而,使用实际上完成部分工作。但是,您不需要 的--is-ancestor 模式git merge-base并且确实需要一些额外的处理。

请注意,无论T与某个祖先之间的路径如何,该祖先的合并基础T(例如AB)要么是祖先本身(AB分别在此处),要么是祖先的某个祖先,例如提交,*如果我们查看TC作为一对。(即使在多个合并基础的情况下也是如此,尽管我将构建一个证明留给你。)

如果测试提交的所有集合中的一个或任意选择的一个合并基础和分支提示还不是测试提交,我们就有类似的情况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. 如果此列表为空,则测试提交首先不是分支提示的祖先。我们有一个类似 commit 的案例C;所以我们有我们的答案。如果列表非空,则将其缩减为其合并组件(使用 再次运行--merges,或将列表提供给git rev-list --stdin --merges,以生成缩小列表)。如果此列表空,请通过查找其--first-parentID 并确保结果在第一个列表中来检查每个合并。

在实际(尽管未经测试)的 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-parentgit rev-list$testcommit$testcommit$branch

hash=$(git rev-parse "$testcommit") || exit 1
t=$(git rev-list --first-parent --topo-order $branch --not ${hash}^@ | tail -1)
test $hash = "$t"

应该做的伎俩。周围的引号$t是为了防止它扩展到空字符串。

于 2017-03-28T21:48:43.760 回答
1

这是一个性能友好的单线:

git rev-parse HEAD~"$( git rev-list --count --first-parent --ancestry-path <commit>..HEAD )"

如果输出是 your <commit>,那么它是第一父祖先。

这个想法是我们用 测量两个提交之间的最短路径rev-list --count --ancestry-path,然后在第一父链中的这个位置获取提交。显然,如果检查的提交是第一父祖先,则这些必须相同。被抑制的错误(例如,第一父链太短)是无关紧要的。

为了使它更复杂,您可以创建一个由可读性很强的 shell 脚本支持的 git 别名。

首先编写脚本文件:

#!/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 别名:

git config --global alias.fp '!<script-path>'

换成fp任何对你来说更舒服的东西。替换<script-path>为您的脚本文件的位置,但保留!字符,有必要使用外部文件。

在此之后,您可以像使用普通 git 命令一样使用新别名:

$ git fp 66e339c
66e339c is a first-parent ancestor of HEAD at a distance of 45
于 2021-08-22T14:13:04.893 回答