如何查询提交是否位于两个已知提交之间?

时间:2016-08-24 23:35:06

标签: git

考虑以下git历史记录:

* commit bfa39b2c6952295dbcfcf8b25667e1965401aed1
| Date:   Wed Aug 24 15:50:14 2016 -0700
|
|     Fix bug introduced by fb77497
|  
* commit ed3d7d82e7c6e6771e0495e3588c5c3089664883
| Date:   Wed Aug 24 15:50:09 2016 -0700
|  
* commit fb77497e8d235d142fca2e18f74895e131b59978
| Date:   Wed Aug 24 15:50:06 2016 -0700
|
|     Massive refactor
|  
* commit be56876ce707df48ca8df683d3f81a11244b9cef
  Date:   Wed Aug 24 15:50:01 2016 -0700

假设提交fb77497引入了一个后来由bfa39b2修复的错误。因此,fb77497ed3d7d8都会受到此错误的影响,但bfa39b2be56876不会。如果历史记录较长,您会说从fb77497(包含)到bfa39b2(不包括)的所有提交都会受到影响。

我可以使用什么命令来确定指定的提交是否在此范围内?如果命令直观地与多个分支一起工作也会很棒。

请注意,bisect命令不适用于此处 - 我已经知道错误的引入和修复位置。

1 个答案:

答案 0 :(得分:3)

有很多方法可以做到这一点;哪一个使用,哪一个是最有效的",取决于其他项目。两种主要方法是使用--ancestry-path,这是git rev-list(因此也是git log)和git merge-base --is-ancestor的选项(自Git版本1.8起可用)。

所以:

git rev-list --ancestry-path A..B

将生成您想要的大部分内容,但它不包括commit A 并包含commit B (请参阅下面的角落案例部分)。这些都是完整的哈希值,因此您现在可以使用grep直接匹配它们只要 grep为完整哈希值(您不希望缩写哈希值a123匹配完整哈希98765a123999...)。

讨论

首先,让我们注意这是一个图形问题。我们有一个DAG,例如:

     o--o--o--o
    /
o--o--o---o--o--o
    \    /       \
     o--o         o
                 /
o--o--o         o

我们选择两个不同的节点 A B 。如果我们选择来自断开连接的子图的节点,则从 A B 路径:

     o--o--o--o
    /
o--o--o---o--B--o
    \    /       \
     o--o         o
                 /
A--o--o         o

所以问题"是 A B 之间某条路径上的任何其他节点"因为没有这样的道路,所以没有实际意义。即使没有断开连接的子图,我们仍然可以解决这个问题。如果我们选择AB,请考虑会发生什么:

     o--o--B--o
    /
o--o--o---o--A--o
    \    /       \
     o--o         o
                 /
                o

AB仍然没有路径,也没有从BA的路径。但是,git rev-list A..Bgit rev-list B..A 生成一个提交列表,特别是那些可从第二个提交ID但不能从第一个提交ID中获取的提交。例如,git rev-list A..B将列出已加星标的提交:

     *--*--B--o
    /
o--o--o---o--A--o
    \    /       \
     o--o         o
                 /
                o

加上提交B本身。

因此,我们必须选择 A B ,以便 A B B 是 A 的祖先。 (使用 A != B 来约束它也是合理的,我们在说 distinct 节点时就这样做了。)失去一般性,我们可以假设 A B 的祖先,只需交换 A B 即使不是,如果合适的话:

     o--o--o--o
    /
o--B--o---o--A--o
    \    /       \
     o--o         o
                 /
                o

这里我们简单地交换,以便 A 是左边的提交, B 是右边的那个(我假设历史从左到右移动)在这里,所有箭头指向这个特定的平面图中的向左,或向上和向左或向下和向左)。

现在, A B 可能有多个路径,所以我们感兴趣的是是否任意选择节点 C 位于从 A B 任何祖先路径中:

     o--o--o--o
    /
o--A--*---*--B--o
    \    /       \
     *--*         o
                 /
                o

如果我们为 C 选择一个现在已加星标的节点,答案是&#34;是的, C B的祖先 A &#34;的后代。如果我们为 C 选择任何其他节点,答案应为&#34; no&#34;: C 不是<的祖先em> B ( B 右侧的三个节点,或顶行的任何节点)或 C 不是 A的后代(任何底行节点,或 A 左侧的节点)。

使用git rev-list

git rev-list A..B    # or: git log --format=%H A..B

我们将获得已加星标的提交,加上提交 B 本身(除非您使用--boundary,否则将省略提交 A ,但添加--boundary可以复杂的图形在所有边界提交中都会有点棘手,包括那些在不需要的路径上的提交,所以最好直接检查 A ,或将其添加到列表中。)

这里存在两个缺点:首先,如果 A B 的祖先,我们可能仍会得到一些提交!这是我们之前的示例,B位于图表的顶行。但即使 A B 的祖先,我们也可能太多提交:

     o--o--o--o
    /
o--o--A---*--B--o
    \    /       \
     *--*         o
                 /
                o

这三个已加星标的提交,加上 B 本身就像往常一样git rev-list(或git log)将为简单的A..B选择:提交可以从 B (包括 B 本身),无法从 A 访问。不幸的是,这包括我们不想要的两个提交。

一种解决方案是添加--ancestry-path--ancestry-path选项约束git rev-list的输出以排除不是第一次提交的后代的提交。因此:

git rev-list --ancestry-path A..B

将只生成 B 本身以及以下一个星号提交:

     o--o--o--o
    /
o--o--A---*--B--o
    \    /       \
     o--o         o
                 /
                o

当然,如果 A 本身不是 B 的祖先(或者只有 B <),它也不会产生任何效果。 em> A = B )。

使用git merge-base --is-ancestor

您还可以使用git merge-base --is-ancestor一次测试一次提交。假设我们已经将 A B 按正确顺序放置:

if git-merge-base is-ancestor $C $B && git merge-base is-ancestor $A $C; then
    ... $C is "between" $A and $B
fi

作为一个好的副作用,提交被认为是它自己的祖先,所以这匹配 C = A ,但你仍然需要测试 C = B 丢弃那个案子。

提防角落案件:#1

一直以来,我一直在假设提交 A 是引入错误的那个 B 是修复错误的那个 - 不完全相同你的榜样。您当然只是将一个节点从修复程序中移回到图形中 - 但是如果修复是由于合并而发生的,那么#34;将一个节点向后移动&#34;可能很棘手,因为可能有多个前任提交。例如,假设 B 是图中的错误修复,如下所示:

...--A--o--o--B--...
      \      /
       o----o

在这里,我们可能希望检查 A 之后的每个节点,包括两行中的节点,但B^ B 的第一个父节点)可能在顶行或底行,将排除另一行。

(使用gitrevisions语法有一个技巧,我没有彻底测试过:

git rev-list --ancestry-path ^A B^@

^@后缀表示&#34; B 的所有父母,但不是 B 本身&#34;,^前缀表示&#34;使用 A 作为排除ID&#34;,--ancestry-path似乎使用&#34;所有排除ID;必须是后代&#34;约束。也值得尝试^A^@来包含 A - 它似乎只是在我刚试过的一个太简单的测试中工作。)

提防角落案件:#2

使用--boundary会将提交A添加回由git rev-list生成的列表(标有前导-),但将--boundary与{{{{1}组合在一起1}}无法正常工作。 --ancestry-path标志告诉Git包含边界条件排除的提交,但有时这包括非祖先路径边界提交。所以它适用于提交--boundary本身,但不适用于提交被剪切而不是非祖先路径。有点难以想出一个快速图表示例,而且这个示例未经过测试:

A

此处 o-----o / \ ...--o--A--o--o--B \ / \ / o-o---o--o 尝试修剪底行的不需要的部分,但--ancestry-path可能会放回其中一个不需要的提交。 (我已经看到了这一点,但不记得是什么引发了它,这就是为什么这可能是一个虚假的例子。)