当我们制作一个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.php
与R中的版本匹配,即缩写哈希为617021e8d9
的那个。 但是你不想要任何提交:你想要第一个这样的提交——最接近L的那个。
值得注意的是,首先,两个提交之间可能根本没有合理的关系。 也就是说,如果我们要绘制提交历史记录的图形,它可能很简单:
...--o--o--L--M--N--...--Q--R--o--o--o <-- branch
但事情可能没有那么简单。 目前,让我们假设它很简单。
简单的情况:L是L
,R是R
,中间有一条直线
提交在这种情况下,从L到R有一些直接的因果关系。 您的问题的答案将很有意义。 具体来说,它回答了这个问题:这个版本从何而来?有一行直接的提交从L
开始,到R
结束,R
中的版本也可能在较早的提交中。 让我们看看如何在L
to-R
序列中找到最早的提交,该提交与R
中的版本相同。
首先,请注意,每次提交都表示该快照中所有文件的完整快照。 也就是说,如果我们查看上面的提交N
,它包含某种形式的所有文件。N
中wp-includes/version.php
的副本可能与L
中的副本匹配,也可能与R
中的副本匹配。 (它显然不能同时匹配两者:如果是这样,L
中的那个将与R
中的那个匹配,并且不会有index
行,也没有差异输出。
该文件可能处于L
和R
中,但不在两者之间的任何提交中,但在这种情况下,答案是:文件首先出现在R
中。
该文件也可能是L
和R
的,并且在某些(但不是全部)中间提交中:假设L
拥有它,然后在M
中删除它,然后它以R
的形式再次以N
出现,然后在O
中再次删除, 等等。 所以它存在于L
、N
、P
和R
中;它在M
、O
和Q
中缺失。 现在的问题更困难了:你想在N
中看到它,即使它在O
再次消失了? 还是您只想在R
中看到它,因为它在Q
中丢失了?
无论如何,我们需要做的是枚举L
到R
范围内的所有提交。 因此,我们将从:
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
。
其余部分假设我们正在用sh
或bash
或类似方式编写此脚本。 在我们运行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
或者,图表可能是这样,L和R是截然不同的:
...--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
来测试这些情况:如果两者都不是另一个的祖先,则它们可能通过共同的第三个祖先(两个提交的实际合并基础)相关联,或者它们可能完全不相关。
如果在L
和R
之间有分支,以便在R
或之前进行合并,则遍历可能会以一些难以预测的顺序发生。 为了强制 Git 按拓扑排序顺序枚举提交,我在实际脚本中使用了--topo-order
。 这迫使 Git 一次遍历合并的每个"腿"。 这在这里不一定是关键的,但它使推理脚本的输出更容易。