Git如何找到重复的提交(通过补丁ID)?

13

我需要一份查找重复更改的指南。

使用patch-id可能会得到相同的结果,但提交属性可能不同。

这似乎是patch-id的预期用途:

git patch-id --help

也就是说,您可以使用此工具查找可能重复的提交。

我想通过串联“git log”、“git patch-id”和uniq命令来完成这项工作,但如果有人有一条能够很好地完成任务的命令,我会非常感激。


这是一个非常有趣的功能。出于好奇,您打算回溯多久的历史记录?我可以看到一些创意整合的用途(例如,“我的贡献者不知道如何变基”),但在过长的历史记录中,它可能会变得不那么有效...? - Christopher
问题在单个分支的为期一周的历史记录中出现,所以我的用例相当温和(git log -p就足够了)。但是patch-id评论让我感到好奇...搜索整个历史记录可能会很痛苦。 - bsb
1
git patch-id 现在应该能够在 Git 2.39 (Q2 2022) 中正确地报告所有差异(属性或二进制)。请参见我下面的更新答案 - VonC
7个回答

12

由于重复更改很可能不在同一个分支上(除非它们之间存在还原),您可以使用git cherry命令:

git cherry [-v] [<upstream> [<head> [<limit>]]]

其中upstream将是检查与head中的更改是否有重复的分支。


12

如果你想查找特定提交的重复项,这可能对你有用。

首先,确定目标提交的补丁 ID:

$ THE_COMMIT_REF_OR_SHA_YOURE_SEEKING_DUPES_OF='7a3e67c'
$ git show $THE_COMMIT_REF_OR_SHA_YOURE_SEEKING_DUPES_OF | git patch-id
f6ea51cd6acd30cd627ce1a56e2733c1777d5b52 7a3e67ce38dbef471889d9f706b9161da7dc5cf3

第一个 SHA 是补丁 ID。接下来,列出每个提交的补丁 ID,并过滤掉与之匹配的任何内容:

$ for c in $(git rev-list --all); do git show $c | git patch-id; done | grep 'f6ea51cd6acd30cd627ce1a56e2733c1777d5b52'
f6ea51cd6acd30cd627ce1a56e2733c1777d5b52 5028e2b5500bd5f4637531e337e17b73f5d0c0b1
f6ea51cd6acd30cd627ce1a56e2733c1777d5b52 7a3e67ce38dbef471889d9f706b9161da7dc5cf3
f6ea51cd6acd30cd627ce1a56e2733c1777d5b52 929c66b5783a0127a7689020d70d398f095b9e00

通过一些额外的标志,以及以 实用脚本 的形式,将所有内容组合在一起:

test ! -z "$1" && TARGET_COMMIT_SHA="$1" || TARGET_COMMIT_SHA="HEAD"

TARGET_COMMIT_PATCHID=$(
git show --patch-with-raw "$TARGET_COMMIT_SHA" |
    git patch-id |
    cut -d' ' -f1
)
MATCHING_COMMIT_SHAS=$(
for c in $(git rev-list --all); do
    git show --patch-with-raw "$c" |
        git patch-id
done |
    fgrep "$TARGET_COMMIT_PATCHID" |
    cut -d' ' -f2
)

echo "$MATCHING_COMMIT_SHAS"

使用方法:

$ git list-dupe-commits 7a3e67c
5028e2b5500bd5f4637531e337e17b73f5d0c0b1
7a3e67ce38dbef471889d9f706b9161da7dc5cf3
929c66b5783a0127a7689020d70d398f095b9e00

虽然速度不算太快,但对于大多数存储库来说,应该能完成任务(在2.4GHz Core 2 Duo上测试了一个包含826个提交和大小为158MB的.git文件夹的存储库,仅用时36秒)


我可能是唯一一个困惑的人,但以防万一,“target-commit”不是字面意思;请用您想要获取补丁ID的提交的SHA替换它。 - Jimothy
1
@Jimothy 是的,或者分支名称或标签名称(任何引用,我猜)。我会看看能否让它更清晰明了。 - Slipp D. Thompson

4

我有一个在玩具仓库上运行的草稿,但是由于它将补丁->提交映射保存在内存中,因此在大型仓库上可能会遇到问题:

# print commit pairs with the same patch-id
for c in $(git rev-list HEAD); do \
    git show $c | git patch-id; done \
| perl -anle '($p,$c)=@F;print "$c $s{$p}" if $s{$p};$s{$p}=$c'

输出应该是具有相同补丁ID的提交对(3个重复的A B C会显示为“A B”,然后是“B C”)。
更改git rev-list命令以限制所检查的提交:
git log --format=%H HEAD somefile

将"| xargs git show"添加到命令中可查看详细提交记录, 或者加上"| xargs git show -s --oneline"以获得摘要:

0569473 add 6-8
5e56314 add 6-8 again
bece3c3 comment
e037ed6 add comment again

事实证明,在我的原始案例中,补丁ID并没有起作用,因为后来的提交中还有其他更改。"git log -S" 更加有用。


如果您只想查看提交与其父级之间的未经加工的差异,您可以执行类似于git diff $c〜1 $c | git patch-id的操作。这在合并提交时会出现问题。跟踪两个合并父级是一个更复杂的问题。 - Christopher
看起来patch-id找到了相同的差异?$ git diff HEAD~1 HEAD | git patch-id 3318362fa07e580.. 000000000000.. $ git show HEAD | git patch-id 3318362fa07e580.. c397c4cdc426.. - bsb
@bsb 你确定想要输入 git show $c | git patch-id 吗?git show 只会打印元数据,但是 git patch-id 需要一个补丁作为输入... - Daniel Alder
@daniel-alder,我认为我需要使用show而不是diff,因为这样可以让Perl打印重复的提交(否则我只会得到很多零SHA)。这段代码跳过非差异输入(尽管旧版本可能不支持此功能,你使用的是哪个版本?) - bsb
@bsb 谢谢你的解释。我再次检查并看到了差异。git showgit patch-id似乎很好地合作,但仅适用于普通提交。对于合并,它似乎不显示任何差异,这就是我的问题。在1.7.10.4和1.9.1上进行了测试。 - Daniel Alder

3

要搜索与提交$hash重复的提交,不包括合并提交:

git rev-list --no-merges --all | xargs -r git show | git patch-id \
    | grep ^$(git show $hash|git patch-id|cut -c1-40) | cut -c42-80 \
    | xargs -r git show -s --oneline

如果要查找合并提交$mergehash的重复项,请将上面的$(git show $hash|git patch-id|cut -c1-40)替换为git diff-tree -m -p $mergehash | git patch-id给出的两个补丁ID(第一列)中的一个。它们对应于合并提交与其两个父提交之间的差异。

要查找所有提交的重复项,但排除合并提交:

git rev-list --no-merges --all | xargs -r git show | git patch-id \
    | sort | uniq -w40 -D | cut -c42-80 \
    | xargs -r git log --no-walk --pretty=format:"%h %ad %an (%cn) %s" --date-order --date=iso

搜索重复提交的范围可以通过更改传递给 git rev-list 的参数进行扩展或限制,它接受许多选项。例如,要将搜索限制为特定分支,请指定其名称而不是选项--all;或者要搜索最近的100次提交,请传递参数HEAD ^HEAD~100
请注意,这些命令速度快,因为它们不使用shell循环,并且批量处理提交。
要包括合并提交,请删除选项--no-merges,并将xargs -r git show替换为xargs -r -L1 git diff-tree -m -p。这样做会慢得多,因为git diff-tree每个提交执行一次。
解释:
  • 第一行生成一个补丁ID与提交哈希的映射(两列数据,每列40个字符)。
  • 第二行只保留与重复的修补程序ID(第一列)相对应的提交哈希(第二列)。
  • 最后一行打印有关重复提交的自定义信息。

2

bsb提出的巧妙命令需要进行一些小调整:

(1) 命令应该使用git diff-tree --cc而不是运行git show

    git diff-tree -p

否则,git patch-id 会生成虚假的空SHA1哈希值。
(2)当使用管道到 xargs 时,xargs 应该带有 -L 1 参数。否则,一个重复的提交将无法与等效的提交配对。
以下是要添加到 ~/.gitconfig 中的别名:
dup = "!f() { for c in $(git rev-list HEAD); do git diff-tree -p $c | git patch-id; done | perl -anle '($p,$c)=@F;print \"$c $s{$p}\" if $s{$p};$s{$p}=$c' | xargs -L 1 git show -s --oneline; }; f" # "git dup" lists duplicate commits

0

对于想在Windows PowerShell上执行此操作的任何人,与unagi的答案相当的命令是:

git rev-list --no-merges --all  | %{&git.exe show $_} | 
  git patch-id | ConvertFrom-String -PropertyNames PatchId, Commit | 
  Group-Object PatchId | Where-Object count -gt 1 | 
  %{$_.group.Commit + " "}

输出结果如下:

1605e0e1e13d7b3f456c20432d8edec664ca7117
1e8efa8f2f01962a2c08fd25caf687d330383428

b45b6db084b27ae420ac8e9cf6511110ebb46513
4a2e1e3ba5a9a1d5db1d00343813e1404f6124e2

将重复的提交哈希值分组在一起。

注意:对于我的版本库而言,这是一个较慢的命令,所以请确保适当地过滤 rev-list 的调用!


0

请确保使用最新版本的Git(2.39或更高版本)

OP bsb答案中提到的git log --format=%H并不总是唯一的。

这是因为,在Git 2.29之前(2020年第四季度),补丁ID计算不会忽略“不完整的最后一行”标记,如空格。

请查看 提交 82a6201(2020年8月19日),提交者为René Scharfe (rscharfe)
(由Junio C Hamano -- gitster --于2020年8月24日合并至提交 5122614

patch-id:在diff_flush_patch_id()中忽略文件末尾的换行符

报告者:Tilman Vogel
初始测试者:Tilman Vogel
签名作者:René Scharfe

计算补丁ID时会忽略空格。
这是通过在对差异行进行哈希之前从中删除所有空格(包括文件末尾的换行符)来完成的。
但是如果缺少换行符,则差异报告将在单独的行中报告该事实,其中包含“\ No newline at end of file\n”,并且此标记会像上下文行一样进行哈希。

这与我们使补丁ID独立于空格的目标相违背。

请改用与2485eab55cc(git-patch-id: do not trip over "no newline" markers, 2011-02-17)添加到git patch-id(man)中的启发式方法,跳过以反斜杠和空格开头且长度大于12个字符的差异行。


“补丁 ID”实际上是与补丁相关的差异的 SHA-1,忽略空格和行号。
实际上,git patch-id 将在 Git 2.39(2022 年第四季度)中更新。
新的 "--include-whitespace" 选项已添加到 "git patch-id" 中(man),并且 Git 2.39(2022 年第四季度)中已经纠正了内部 patch-id 逻辑中不匹配 "git patch-id" 的现有错误。

请查看 提交 0d32ae8, 提交 2871f4d, 提交 93105ab, 提交 0df19eb, 提交 51276c1, 提交 0570be7 (2022年10月24日) 由Jerry Zhang (jerry-skydio)提交。
(由Taylor Blau -- ttaylorr --合并于提交 160314e, 2022年10月30日)

builtin:patch-id: 添加--verbatim作为命令模式

署名:Jerry Zhang
署名:Junio C Hamano

有些情况下,用户可能不想使用默认设置,其中patch-id会剥离所有空格。他们可能正在使用空格在语法上很重要的语言,或者他们可能有CI测试强制执行严格的空格linting。在这些情况下,空格更改将导致补丁根本变化,因此应具有不同的ID。
添加一个新的模式,它与--stable和--unstable相互独立,称为--verbatim。它也对应于config - patchid.verbatim = true。在此模式下,使用稳定算法,并且不从补丁文本中剥离空格。
用户git --unstable主要关心与旧版本的兼容性,去除空格会破坏这种兼容性。因此,不存在--verbatim和--unstable的组合用例,我们不公开此使用场景,以避免增加维护负担。
修复https://github.com/Skydio/revup/issues/2

git patch-id现在在其手册页面中包括:

--verbatim

计算输入的patch-id,不要去掉任何空格。

如果patchid.verbatim为true,则这是默认设置。

但不仅如此。
来自OP的内容:

我想要一个查找重复更改的方法。 patch-id可能相同,但提交属性可能不同。

这也在Git 2.39中得到了修复:

patch-id:修复针对模式更改的patch-id

已签署:Jerry Zhang

目前在rebase和cherry-pick中使用的patch-id如果文件被修改,则不考虑文件模式。
这样做的一个后果是,如果您有一个本地补丁更改了模式,但上游已经应用了一个过时的补丁版本,该版本不包括该模式更改,那么"git rebase"(man)将删除您的本地补丁版本以及您的模式更改。
这也意味着内部patch-id与内置的不会产生相同的输出,因为它们是diff输出的一部分,而内置的会考虑到模式更改。
通过在patch-id中添加模式(如果已更改),并以与diff生成的格式相同的方式进行修复,以使其与内置的patch-id兼容。
最后一个未正确检测/报告的差异:

builtin:patch-id:修复二进制差异的 patch-id

署名:Jerry Zhang

"git patch-id"(man) 目前在处理包含二进制文件的差异时无法正确输出。
get_one_patchid中添加逻辑,以处理可能出现的不同二进制差异样式。
此举旨在保持生成的补丁 ID 与diff.c中对应逻辑产生的补丁 ID 相同,即通过依次哈希ab的对象 ID 来生成补丁 ID。

一般情况下,我们通过首先缓存“索引”行中的对象 ID,并在找到差异为二进制的指示后使用这些对象 ID 来处理二进制差异。

输入可能包含使用 "git diff --binary"(man)" 生成的补丁。
目前这会破坏解析逻辑,并导致一个提交对应多个补丁 ID 的输出。
在这种情况下,我们需要跳过补丁本身的内容,因为它们不会被纳入补丁 ID 中。
--binary imply --full-index,所以对象 ID 总是可用的。

当使用--full-index生成差异时,没有要跳过的补丁内容。

当使用--full-index--binary以外的选项生成差异时,对象 ID 将会被缩写。
虽然在哈希后仍会产生足够唯一的补丁 ID,但与内部补丁 ID 输出不匹配。
目前我们将其视为可接受,因为为了匹配内部补丁 ID,我们已经需要特定的 diff 参数(即-U3)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接