为什么git怪不跟着重命名?

Why git blame does not follow renames?

$ 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: 那是不可能的。阅读下文。

来自 MAILLIST 的回答 作者:Junio C Hamano

git blame follow renames but not for 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" 跟随内容移动,从不对待 "renames" 任何特殊的方式,因为认为重命名是愚蠢的事情 有点特别 ;-)

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

git blame 是否遵循重命名(如果您给它 --followgit log 也会如此)。问题在于重命名之后的方式,这是一个not-very-thorough hack:因为它一次退回一个提交(从每个child到每个parent), 它会产生差异——你可以手动产生相同类型的差异:

git diff -M SHA1^ SHA1

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

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

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

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

其中每个大写字母代表一次提交。进一步假设文件在提交 R 中被重命名,因此在提交 RT 中它的名称为 newname 而在提交 A 到 [=24 中=] 它的名字是 oldname.

如果你运行git blame -- newname,序列从T开始,比较ST,比较RS,并比较 QR比较QR时,git blame发现name-change,并开始在提交中寻找oldnameQ 和更早版本,所以当它比较 PQ 时,它会比较这两个提交中的文件 oldnameoldname

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

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

git blame R^ -- oldname

然后一切正常。


1git diff documentation中,你会看到有一个-M选项控制如何 git diff 检测重命名。 blame 代码稍微修改了这一点(实际上做了两次传递,一次关闭 -M,第二次打开 -M)并使用它自己的(不同的)-M 选项用于不同的目的,但最终它使用相同的代码。


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

Is any tool that can show me file renames like: git renames <filename> SHA date oldname->newname

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

我不确定这里的 "SHA date" 是什么意思,但是 git diff -M 允许您提供两个 SHA-1 并比较 left-vs-right。添加 --name-status 以仅获取文件名和配置。因此 git diff -M --name-status HEAD oldsha1 可能 报告要从 HEAD 转换为 oldsha1,git 认为您应该 R 命名一个文件并将旧名称报告为 "new" 名称。例如,在 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 所做的那样,通过每次提交进行一系列重命名的事实可能会产生与比较 much-later 提交完全不同的答案(HEAD) 针对 much-earlier 提交(992cb206 或其他)。在这种情况下,结果是一样的,但这里的 "similarity index" 是 100 分中的 97 分。如果在某些中间步骤中对其进行更多修改,则该相似度指数可能会低于 50% .. . 然而,如果我们要比较 992cb206992cb206 之后的 little 的修订版(就像 git blame 那样),也许两者之间的相似性指数这两个文件可能更高。

git rev-list 本身需要(和缺少的)来实现 --follow,以便所有在内部使用 git rev-list 的命令——即,大多数命令不仅仅用于一次修订——可以解决问题。一路上,如果它在另一个方向上工作会很好(目前 --follow 仅 newer-to-older,即,与 git blame 一起工作,与 git log 一起工作只要您不首先使用 --reverse).

询问最古老的历史记录

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

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

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

现在您可以:

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

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

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

Continue listing the history of a file beyond renames (works only for a single file).

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

Detect copies as well as renames. See also --find-copies-harder. If n is specified, it has the same meaning as for -M.

-C 将在同一提交中查看不同的文件。如果你想检测代码是从不同的文件中获取的,而这个文件在这次提交中没有改变。那么你应该提供 --find-copies-harder 选项。

For performance reasons, by default, -C option finds copies only if the original file of the copy was modified in the same changeset. This flag makes the command inspect unmodified files as candidates for the source of copy. This is a very expensive operation for large projects, so use it with caution. Giving more than one -C option has the same effect.

UPD
我改进了这个别名:

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

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

git follow file_name.c 30 35

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

git follow old_file_name.c 30 35 85ce061

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

注意:很遗憾,git 没有考虑工作目录中的更改。因此,如果您对文件进行本地更改,则必须先将其存储起来 follow changes