49
$ 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>

更新2:那是不可能的。参见下文。

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的内容?当你在这个命令中告诉它 C 时,你是如何告诉你的意思是 A 而不是 B?

“git blame”跟随内容运动,从不以任何特殊方式对待“重命名”,因为认为重命名有某种特殊性是一件愚蠢的事情;-)

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

4

2 回答 2

36

git blame 确实遵循重命名(就像git log你给它一样--follow)。问题在于它遵循重命名的方式,这是一个不太彻底的 hack:当它一次退回一个提交时(从每个孩子到每个父母),它会产生一个差异——你可以使用相同类型的差异手动制作:

git diff -M SHA1^ SHA1

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

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

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

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

其中每个大写字母代表一个提交。进一步假设一个文件在 commit 中被重命名R,因此在R通过T它的提交中具有 namenewname而在A通过Q它的提交中具有 name oldname

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

另一方面,如果您运行git blame R^ -- newname(or git blame Q -- newname) 以使序列从 commit 开始Q,则该提交中没有文件,并且在比较andnewname时没有重命名,并且简单地放弃了。PQgit blame

诀窍是,如果您从文件具有先前名称的提交开始,则必须为 git 提供旧名称:

git blame R^ -- oldname

然后一切都重新开始了。


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


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

是否有任何工具可以显示文件重命名,例如: git renames <filename> SHA date oldname->newname

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

我不确定您在这里所说的“SHA 日期”是什么意思,但git diff -M允许您提供两个 SHA-1 并比较左右。添加--name-status以获取文件名和配置。因此git diff -M --name-status HEAD oldsha1 可能会报告要从HEADto转换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进行比较的完全不同的答案。在这种情况下,结果是一样的,但是这里的“相似度指数”是 97 分(满分 100)。如果在某些中间步骤中对其进行更多修改,那么相似度指数可能会低于 50% ......但是,如果我们要在稍后比较一个修订版(就像那样),那么这两个文件之间的相似性指数可能会更高。HEAD992cb206992cb206992cb206git blame

需要(和缺少)的是git rev-list它自己来实现--follow,以便所有git rev-list内部使用的命令——即,大多数在不止一个版本上工作的命令——都可以做到这一点。一路走来,如果它在另一个方向上工作会很好(目前--follow只是从新到旧,即,只要你不首先询问最古老的历史,它就可以正常工作git blame并且可以正常工作)。git log--reverse

于 2015-04-06T09:46:15.040 回答
3

**见 UPD。现在您可以关注重命名的文件

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

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

现在你可以:

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

您将看到更改<filename>.

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

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

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

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

-C将在同一提交中查看不同的文件。如果您想检测代码是从在此提交中未更改的不同文件中获取的。然后你应该提供--find-copies-harder选项。

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

UPD
我改进了这个别名:

[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 follow file_name.c 30 35

即使您可以继续关注以提交 (@arg4) 开头的不同文件

git follow old_file_name.c 30 35 85ce061

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

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

于 2018-01-26T13:26:32.990 回答