如何通过添加文件索引(blob)找到负责的提交

How to find commit responsible by adding a file index (blob)

当我们制作 git diff Version1..Version2 -- file 时,此命令将 return 类似于:

diff --git a/wp-includes/version.php b/wp-includes/version.php index 5d034bb9d8..617021e8d9 100644

这里的git比较一个文件的两个版本,让您知道它们之间的区别。 我需要从索引号 5d034bb9d8 和索引 **617021e8d9*.

中知道负责添加相关文件的提交

TL;DR

这个(未经测试的)脚本可能会做你想做的事。阅读其余部分以了解其工作原理、是否有效以及何时有效以及注意事项。

#! /bin/sh
case $# in
2);;
*) echo "usage: script left-specifier right-specifier" 1>&2; exit 1;;
esac
# turn arguments into hashes, then ensure they are commits
L=$(git rev-parse "") || exit
R=$(git rev-parse "") || exit
L=$(git rev-parse $L^{commit}) || exit
R=$(git rev-parse $R^{commit}) || exit

haveblob=$(git rev-parse $L:wp-includes/version.php) || exit
wantblob=$(git rev-parse $R:wp-includes/version.php) || exit
git rev-list --reverse --topo-order $R ^$L^@ | while read hash; do
    thisblob=$(git rev-parse $hash:wp-includes/version.php)
    test $thisblob = $haveblob && continue
    if [ $thisblob = $wantblob ]; then
        echo "target file appears in commit $hash"
        exit 0 # we've found it - succeed and quit
    fi
    echo "note: commit $hash contains a different version than either end"
done
echo "error: got to the bottom of the loop"
exit 1

让我们再澄清一点:你 运行:

$ git diff <commit1> <commit2> -- wp-includes/version.php

其输出部分为:

index 5d034bb9d8..617021e8d9 100644

让我们调用 <commit1>——你通过散列或标签或分支名称或其他任何方式指定的——L,其中 L代表 git diff 左侧。我们将右侧的第二个提交称为 R

您想找到在 L 或之后以及 R 之前或之后的提交,其中文件 wp-includes/version.php 匹配 R 中的版本,即缩写哈希为 617021e8d9 的版本。但是你不想要 any 提交:你想要 first 这样的提交——最接近 L[=205 的那个=].

首先,值得注意的是,这两个提交之间可能根本没有任何合理的关系。也就是说,如果我们要绘制提交历史图,它可能很简单:

...--o--o--L--M--N--...--Q--R--o--o--o   <-- branch

但这可能不是那么简单。目前,我们假设它很简单。

简单情况:LLRR,有一条直线在

之间提交

在这种情况下,从LR有一些直接的因果关系。您的问题的答案将很有意义。具体来说,它回答了这个问题:这个版本是从哪里来的? 有一个直接的提交行从 L 开始并在 R 结束,并且版本在R 也可能在较早的提交中。让我们看看如何在 L-to-R 序列中找到最早的提交,它具有 相同的 版本 R.

首先,请注意每次提交都代表该快照中所有文件的完整快照。也就是说,如果我们查看上面的提交 N,它会以某种形式包含所有文件。 Nwp-includes/version.php 的副本可能与 L 中的副本匹配,也可能与 R 中的副本匹配。 (它显然不能同时匹配两者:如果匹配,L 中的那个将匹配 R 中的那个并且将没有 index 行并且没有差异输出。)

文件可能在 LR 中,但不在两者之间的 any 中,但在这种情况下,答案是:文件首先出现在R.

也有可能文件在LR中,在some中,但不在all[=205=中], 中间提交:说 L 有它,然后它在 M 中被删除,然后它以它在 R 中的形式再次出现在 N 中,然后它是在 O 中再次删除,依此类推。所以它出现在 LNPR 中;它在 MOQ 中缺失。现在问题更难了:你想在 N 中看到它,即使它在 O 中又消失了吗?还是您只想在 R 中看到它,因为它在 Q 中不见了?

无论如何,我们需要做的是枚举LR范围内的所有提交。所以我们将从:

git rev-list L..R

(这将省略 L,这有点烦人)。 Git 将以 reverse-ish 顺序枚举这些;因为我们知道链是线性的,所以这实际上是直接逆序。 (稍后我们将看到如何为更复杂的情况强制执行合理的顺序。)要检查 L 本身,我们可以直接添加它:

(git rev-list L..R; git rev-parse L)

或者我们可以使用相当复杂的技巧:

lhash=$(git rev-parse L); git rev-list R ^${lhash}^@

(详情见the gitrevisions documentation)。越简单的:

git rev-list L^..R

通常也能正常工作:只有当 L 是根提交时才会失败。

无论如何,git rev-list的输出是一堆提交哈希ID:提交R的哈希ID,然后是提交Q的哈希ID,然后是提交的哈希ID P,依此类推,一直回到L。因此,我们将通过命令传输此 git rev-list 的输出,以确定我们的特定 blob 来自何处。但是我们想以其他顺序访问提交:首先是 L,然后是 M,然后是 N,一直到 R。所以我们将 --reverse 添加到 git rev-list 参数。

其余部分假定我们在 shbash 或类似格式中编写此脚本。在我们 运行 git rev-list 之前,让我们先获取文件每个版本的完整 blob-hash。然后我们会让他们进入循环:

#! /bin/sh
case $# in
2);;
*) echo "usage: script left-specifier right-specifier" 1>&2; exit 1;;
esac
# turn arguments into hashes, then ensure they are commits
L=$(git rev-parse "") || exit
R=$(git rev-parse "") || exit
L=$(git rev-parse $L^{commit}) || exit
R=$(git rev-parse $R^{commit}) || exit

# get the blob hashes, exit if they don't exist
haveblob=$(git rev-parse $L:wp-includes/version.php) || exit
wantblob=$(git rev-parse $R:wp-includes/version.php) || exit
git rev-list --reverse $R ^$L^@ | while read hash; do
    ...
done

在循环内,让我们获取此提交的 blob 哈希:

    thisblob=$(git rev-parse $hash:wp-includes/version.php)

如果此失败,则表示文件已删除。我们可以选择忽略它并通过添加 || continue 或停止 || break 来跳过此提交,或者我们可以简单地完全忽略这种可能性,假设该文件将存在于每次提交中。由于最后一个最简单,所以我就在这里做。

如果这个散列匹配$haveblob,它这不是很有趣。如果匹配到$wantblob,就很有意思了。如果它完全是另一回事,那么,让我们把它说出来。所以循环的剩余部分是:

    test $thisblob = $haveblob && continue
    if [ $thisblob = $wantblob ]; then
        echo "target file appears in commit $hash"
        exit 0 # we've found it - succeed and quit
    fi
    echo "note: commit $hash contains a different version than either end"

这是顶部的脚本(好吧,大部分)。

更复杂的案例需要更多注意事项

图表内部可能更 branch-y; R 甚至可以是 合并提交:

       M-----N
      /       \
...--L         R   <-- branch
      \       /
       O--P--Q

或后一个:

       M--N
      /    \
...--L      Q--R   <-- branch
      \    /
       O--P

或者,图表可能是 LR 截然不同:

...--o--o--o--L--o--o   <-- branch1
      \
       o--...--o--R--o   <-- branch2

或者(如果有多个根提交)它们甚至可以完全不相关,graph-wise:

A--B--L   <-- br1

C--D--R   <-- br2

或者,它们可能是相关的,不管是不是简单的线性关系,但是向后:

...--o--R--E--F--G--L--o--...--o   <-- branch

如果这两个提交像这样向后,你应该简单地交换它们。 (脚本可以这样做:git merge-base --is-ancestor A B 测试提交 A 是否是提交 B 的祖先。)

如果它们不直接相关,L..R 语法将排除可从 L 到达的提交,同时列出可从 R 到达的提交。如果它们完全不相关,则从 R 可访问的提交将无法从 L 访问,因此这只是 "all commits in the history up to R"。在任何一种情况下,您都可能找到答案,也可能找不到答案,也可能没有任何意义。

您可以使用上面的 git merge-base 来测试这些情况:如果两者都不是对方的祖先,则它们可能通过共同的第三个祖先相关——实际合并两个提交的基础——或者它们可能完全不相关。

如果有分支"between" LR,使得在R或之前有合并,遍历可能发生在difficult-to-predict ] 命令。为了强制 Git 以 topologically-sorted 顺序枚举提交,我在实际脚本中使用了 --topo-order。这迫使 Git 一次遍历合并的每个 "leg"。这在这里不一定很重要,但它可以更轻松地推理脚本的输出。