为什么 git blame 不遵循重命名


$ pwd
/data/mdi2/classes
$ git blame -L22,+1 -- utils.js
99b7a802 mdi2/utils.js (user 2015-03-26 21:54:57 +0200 22)  #comment
$ git blame -L22,+1 99b7a802^ -- utils.js
fatal: no such path mdi2/classes/utils.js in 99b7a802^

正如您所注意到的,该文件在该提交中的不同目录中

$ git blame -L22,+1 99b7a802^ -- ../utils.js
c5105267 (user 2007-04-10 08:00:20 +0000 22)    #comment 2

尽管在文档上

The origin of lines is automatically followed across whole-file renames (currently there is no option to turn
       the rename-following off)

责备不遵循重命名。为什么?

更新:简答

git blame遵循重命名,但不遵循git blame COMMIT^ -- <filename>

但是,通过大量重命名和大量历史记录手动跟踪文件重命名太难了。我认为,必须修复此行为以静默地遵循git blame COMMIT^ -- <filename>重命名。或者,至少,必须实施--follow,所以我可以:git blame --follow COMMIT^ -- <filename>

UPDATE2:那是不可能的。请阅读下文。

来自邮件列表的答案 作者:Junio C Hamano

git blame遵循重命名,但不遵循git blame COMMIT^ -- <filename>

假设您的版本 v1.0 中有文件 A 和文件 B。

六个月后,代码被重构了很多,你这样做了。不需要分别提供这两个文件的内容。 你有删除了 A 和 B,他们拥有的大部分内容现在都在文件 C 中。 那是当前状态。

git blame -C HEAD -- C

可以遵循两者的内容就好了,但如果你是允许说

git blame v1.0 -- C

这甚至意味着什么? C 根本不存在 v1.0。 你是要求遵循A的内容,还是B? 你怎么告诉你的意思是 A 而不是 B,当你在这个命令中告诉它 C 时?

"git blame"遵循内容移动,从不处理"重命名"任何特殊的方式,因为认为重命名是一件愚蠢的事情有点特别;-(

您告诉从哪些内容开始挖掘命令的方式从其命令行是给出起点提交(默认为HEAD,但您可以以提交^为例(及其中的路径起点。 因为告诉 C 到 Git 和 Git 没有任何意义然后神奇地让它猜到你在某些情况下的意思是 A,在某些情况下是指 B其他。 如果 v1.0 没有 C,唯一明智的做法是退出而不是猜测(并且不告诉用户如何猜测(。

git blame确实遵循重命名(如果你给它--followgit log也是如此(。 问题在于它遵循重命名的方式,这是一个不太彻底的黑客:当它一次退回一个提交(从每个子级到每个父级(时,它会进行差异 - 您可以手动进行的差异:

git diff -M SHA1^ SHA1

— 并检查此差异是否检测到重命名。1

就目前而言,

这一切都很好,但这意味着要让git blame检测到重命名,(a( git diff -M必须能够检测到它(幸运的是,这里就是这种情况(,并且 - 这是导致您问题的原因 - 它必须跨重命名

例如,假设提交图看起来有点像这样:

A <-- B <-- ... Q <-- R <-- S <-- T

其中每个大写字母代表一个提交。 进一步假设一个文件在提交R中被重命名,因此在提交RT中它有名称newname而在提交AQ它有名称oldname

如果运行git blame -- newname,则序列从T开始,比较ST,比较RS,比较QR它比较QR时,git blame 会发现名称更改,并开始在提交Q及更早的提交中查找oldname,因此当它比较PQ时,它会比较这两个提交中oldnameoldname的文件。

另一方面,如果您运行git blame R^ -- newname(或git blame Q -- newname(以使序列从提交Q开始,则该提交中没有文件newname,并且在比较PQ时没有重命名,并且git blame只是放弃。

诀窍是,如果你从文件具有以前名称的提交开始,你必须给 git 一个旧名称:

git blame R^ -- oldname

然后一切又开始了。


1git diff文档中,您将看到有一个-M选项,用于控制git diff如何检测重命名。 blame代码对此进行了一些修改(实际上进行了两次传递,一次关闭了-M,另一次打开了-M(,并使用自己的(不同的(-M选项用于不同的目的,但最终它使用相同的代码。


[编辑以添加对评论的回复(不适合作为评论本身(]:

是否有任何工具可以向我显示文件重命名,例如:git 重命名<文件名>SHA 日期旧名称->新名称

不完全是,但git diff -M接近,可能足够接近。

我不确定您在这里所说的"SHA 日期"是什么意思,但git diff -M允许您提供两个 SHA-1 并比较左右。 添加--name-status以仅获取文件名和处置。 因此,git diff -M --name-status HEAD oldsha1可能会报告要从HEAD转换为oldsha1,git 认为您应该R命名文件,并将旧名称报告为"新"名称。 例如,在 git 存储库本身中,当前有一个名为 Documentation/giteveryday.txt 的文件,它的名称曾经略有不同:

$ git diff -M --name-status HEAD 992cb206
M       .gitignore
M       .mailmap
[...snip...]
M       Documentation/diff-options.txt
R097    Documentation/giteveryday.txt   Documentation/everyday.txt
D       Documentation/everyday.txto
[...]

如果这是你关心的文件,你很好。 这里的两个问题是:

  • 寻找SHA1:992cb206从何而来? 如果您已经拥有 SHA-1,这很容易;如果没有,git rev-list是 SHA1 查找工具;阅读其文档;
  • 事实上,在每次提交进行一系列重命名之后,一次一次提交一次,就像git blame所做的那样,可能会产生与比较更晚的提交(HEAD(与更早的提交(992cb206或其他什么(完全不同的答案。 在这种情况下,结果相同,但这里的"相似性指数"是 97 分(满分 100 分(。 如果在某些中间步骤中对其进行更多修改,则相似性指数可能会降至50%以下......然而,如果我们在992cb206后一点点比较修订版992cb206版(就像git blame一样(,也许这两个文件之间的相似性指数可能会更高。

需要(和缺少(的是git rev-list本身实现--follow,以便所有在内部使用git rev-list的命令 - 即,大多数命令在多个修订版上工作 - 都可以做到这一点。 在此过程中,如果它在另一个方向上工作会很好(目前--follow只是从新到旧的,即,只要您不首先要求最旧的历史记录git log就可以git blame工作正常--reverse(。

请参阅更新。现在,您可以关注重命名的文件。

最新的 git 有有趣的命令。在您的配置旁边添加:

[alias]
    follow= "!sh -c 'git log --topo-order -u -L $2,${3:-$2}:"$1"'" -

现在,您可以:

$git follow <filename> <linefrom> [<lineto>]

您将看到每个提交都会更改<filename>中的指定行。

您也可以对git log命令的选项感兴趣--follow

继续列出重命名以外的文件历史记录(仅适用于单个文件(。

如果您对复制检测感兴趣,请使用-C

检测副本以及重命名。另请参阅--find-copyies-harder。如果指定了 n,则它与 -M 的含义相同。

-C在同一提交中看起来会有不同的文件。如果要检测代码是从此提交中未更改的其他文件中获取的。然后,您应该提供--find-copies-harder选项。

出于性能原因,缺省情况下,仅当副本的原始文件在同一变更集中被修改时,-C 选项才会查找副本。此标志使命令将未修改的文件检查为副本源的候选项。对于大型项目来说,这是一项非常昂贵的操作,因此请谨慎使用。提供多个 -C 选项具有相同的效果。

更新
我改进了这个别名:

[alias]
follow = "!bash -c '                                                 
    if [[ $1 == "/"* ]]; then                                      
        FILE=$1;                                                     
    else                                                             
        FILE=${GIT_PREFIX}$1;                                        
    fi;                                                              
    echo "git log --topo-order -u -L $2,${3:-$2}:\"$FILE\" $4 ";   
    git log -w -b -p --ignore-blank-lines --topo-order -u -L $2,${3:-$2}:"$FILE" $4;
' --"

作为参考,以下是此别名的位:

  • git 别名

    GIT_PREFIX设置为通过从原始当前目录运行git rev-parse --show-prefix返回。

  • --topo-order将按开发轨道对提交进行排序,而不是按日期排序,并可能混合两个不同的轨道。

  • -u-p--patch 相同,所以这将生成一个补丁

  • -L线路范围

  • -w--ignore-all-space 相同,忽略空格差异

  • -b忽略空间更改。

  • -p,生成一个与-u--patch相同的补丁

  • --ignore-blank-lines

现在,您可以跟踪指定范围的行是如何更改的:

git follow file_name.c 30 35

即使你可以继续关注不同的文件,从提交(@arg4(开始

git follow old_file_name.c 30 35 85ce061

85ce061 - 是文件重命名的提交

注意:不幸的是,git 没有考虑工作目录中的更改。因此,如果您对文件进行本地更改,则必须将其存储起来,然后才能follow更改

最新更新