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



当我们制作一个git diff Version1 时。版本2 -- 文件,此命令将返回类似以下内容:

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

这里的 git 比较文件的两个版本,以为您提供它们之间的区别。 我需要知道负责从索引5d034bb9d8 和索引 **617021e8d9* 的数量中添加有问题的文件的提交。

TL;博士

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

#! /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 "$1") || exit
R=$(git rev-parse "$2") || 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之前或R处的提交,其中文件wp-includes/version.phpR中的版本匹配,即缩写哈希为617021e8d9的那个。 但是你不想要任何提交:你想要第一个这样的提交——最接近L的那个。

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

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

但事情可能没有那么简单。 目前,让我们假设它很简单。

简单的情况:LL,RR,中间有一条直线

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

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

该文件可能处于LR中,但不在两者之间的任何提交中,但在这种情况下,答案是:文件首先出现在R中。

该文件也可能是LR的,并且在某些(但不是全部)中间提交中:假设L拥有它,然后在M中删除它,然后它以R的形式再次以N出现,然后在O中再次删除, 等等。 所以它存在于LNPR中;它在MOQ中缺失。 现在的问题更困难了:你想在N中看到它,即使它在O再次消失了? 还是您只想在R中看到它,因为它在Q中丢失了?

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

git rev-list L..R

(这将省略L,这有点烦人)。 Git 将以相反的顺序枚举这些;由于我们知道链是线性的,这实际上是直接的反向顺序。 (稍后我们将看到如何对更复杂的情况强制执行合理的命令。 为了检查L本身,我们可以显式添加它:

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

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

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

(有关详细信息,请参阅 gitrevisions 文档)。 更简单:

git rev-list L^..R

通常也可以工作:它仅在L是根提交时才失败。

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

其余部分假设我们正在用shbash或类似方式编写此脚本。 在我们运行git rev-list之前,让我们获取文件每个版本的完整 blob 哈希。 然后我们将让它们在循环中:

#! /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 "$1") || exit
R=$(git rev-parse "$2") || 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"

这就是顶部的脚本(嗯,主要是)。

更复杂的案例引入了更多的警告

该图在内部可能是相当分支的;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

或者(如果有多个根提交)它们甚至可能是完全不相关的,在图形方面:

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访问,因此这只是"历史记录中截至R的所有提交"。 无论哪种情况,您都可能会或可能不会找到答案,并且可能有意义,也可能没有任何意义。

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

如果在LR之间有分支,以便在R或之前进行合并,则遍历可能会以一些难以预测的顺序发生。 为了强制 Git 按拓扑排序顺序枚举提交,我在实际脚本中使用了--topo-order。 这迫使 Git 一次遍历合并的每个"腿"。 这在这里不一定是关键的,但它使推理脚本的输出更容易。

最新更新