Git能够追踪一个函数从一个文件到另一个文件的移动吗?如何实现?

80
有几次,我看到过这样一种说法:如果你把一个函数从一个文件移到另一个文件,Git就可以追踪它。例如,此条目说,“Linus说,如果你将一个函数从一个文件移动到另一个文件,Git会告诉你跨越移动的那个单独函数的历史记录。”但是我对Git底层设计有一点了解,我不明白这是如何可能的。所以我想知道...这是正确的说法吗?如果是,那么这是怎么做到的?我的理解是,Git将每个文件的内容存储为Blob,并且每个Blob都有一个全局唯一的标识符,该标识符由其内容和大小的SHA哈希值生成。然后,Git将文件夹表示为树形结构。任何文件名信息都属于Tree,而不属于Blob,因此文件重命名会显示为对Tree的更改,而不是对Blob的更改。因此,如果我有一个名为“foo”的文件,其中包含20个函数,以及一个名为“bar”的文件,其中包含5个函数,然后我将其中一个函数从foo移动到bar(分别得到19和6),Git如何检测到我将该函数从一个文件移动到另一个文件?据我所知,这将导致2个新的Blob存在(一个用于修改后的foo,一个用于修改后的bar)。我意识到可以计算差异以显示函数是从一个文件移动到另一个文件的,但我不明白如何将与函数有关的历史记录与bar关联起来,而不是foo(至少不是自动的)。
如果Git实际上查看单个文件的内部,并计算每个函数的一个blob(这将是疯狂/不可行的,因为您必须知道如何解析任何可能的语言),那么我可以看到这可能是可能的。那么...这个说法是正确的还是不正确的?如果正确,那么我的理解缺少什么?

2
我认为它不是跟踪“函数”,而是跟踪“代码块”——因此,如果您有一个30行的函数,并将其分成两个15行的函数,它将以与移动整个函数相同的方式跟踪它。如果我错了,请有人纠正我。 - Tyler
1
我的理解(可能是错误的,所以我才问)是每个文件最多对应一个 Blob。因此,在同一文件中将一个函数拆分为两个较小的函数只会导致旧 Blob 被新 Blob 替换。如果这是正确的,那么它实际上并没有跟踪“代码块”,因为它从不查看文件内部。换句话说,它的最小粒度是整个文件。 - Charlie Flowers
如果你只是将文件分成两个(或多个)部分,那么可以欺骗两个(或多个)分支中的移动指针指向同一个旧文件,这样当你合并这两个分支时,你会得到相同的文件“重命名两次”(或更多次),意味着两个或更多文件具有相同的移动祖先。但仅仅是从一个大文件中移动一小段到另一个大文件中,这种技巧是行不通的,正如你所观察到的那样。只有基于AST的(通常是特定于语言的)工具才能以高精度跟踪重构。 - Fizz
另外,正如下面某个答案所说,从技术上讲并不存在父文件指针,但如果你查看 gitk,在同一次提交中重命名和更改文件时,会出现类似于“相似度 95% 从 src/foo.txt 重命名为 src/bar.txt”的内容。这是来自于 git-diff-index 后端的信息。因此它通过(高)文本相似性来跟踪移动。基本上,为了帮助 git 跟踪重命名操作,你需要使用尽可能少的更改的中间提交,除了文件重命名之外。 - Fizz
如果您想将文件的一小部分移动到新文件中,可以执行以下步骤:(1) 分支,(2) 重命名,(3) 提交[非常重要],(4) 删除文件的大部分,只留下感兴趣的小块,(5) 再次提交,(6) 合并回主干分支。这有效地创建了一个正确的“文件移动”指针,因为有一个高文本相似度的提交(在第3步创建),如果文件在同一提交中未重命名,则git没有问题跟踪任何数量的已删除材料(在第5步创建)。 - Fizz
显示剩余2条评论
5个回答

35

此功能是通过git blame -C <file>提供的。

-C选项驱动git尝试在正在审查的文件中添加或删除文本块与相同变更集中修改的文件之间寻找匹配项。附加-C -C-C -C -C可扩展搜索范围。

在测试仓库中尝试使用git blame -C,您将看到您刚刚移动的代码块起源于原本它所属的原始文件中。

git help blame手册页中得知:

行的起源会自动跨整个文件重命名 (目前没有关闭重命名跟踪的选项)。要跟踪从一个文件移到另一个文件的行,或跟踪从另一个文件复制和粘贴的行等,请参阅-C-M选项。


@Johann,看起来使用普通的 git blame 命令就可以了。 - andrybak
@andrybak 四年过去了,所以我不记得当时真正想要实现什么了。但是 git blame 只会显示该行的最新更改(无论是否移动),而我的评论要求“最近一次提交_移动该行_”(假设在对该行进行更改后进行了更多提交)。 - Johann
3
git version 2.15.0.rc0 中,似乎 -CC-CCC 无法正常工作。为了实现文档中的效果,我需要单独多次传递隔离的 -C 开关。文件说明有点含糊,至少是隐含地指示了这一点。然而,这个答案和其他评论表明过去这个功能是可以正常工作的。嗯。 - underscore_d
这在一个单人店之外并不是非常有用,因为:“您可以使用-M和-C等选项调整git blame算法,以使其尝试更加努力,但实际上,您通常无法控制这些选项:git blame可能在服务器上执行,并将结果报告回您的网页。”(继续) - Fizz
或者git blame是由坐在另一张桌子前的开发人员执行的(你无法控制其命令行选项),而可怜的Greg必须处理所有被分配给他的票据,这些票据来自使用git blame输出找出引起问题的行是谁引入的人。因此,这种方法的问题在于完全依赖于git clone的“接收端”来弄清楚发生了什么,复制了什么以及由谁复制。 - Fizz
显示剩余2条评论

21

截至Git 2.15,git diff现在支持使用--color-moved选项检测移动的行。它适用于跨文件移动。

显然,它适用于带颜色的终端输出。据我所知,在普通文本补丁格式中没有指示移动的选项,但这是有道理的。

要使用默认行为,请尝试

git diff --color-moved

该命令还接受选项,目前包括nodefaultplainzebradimmed_zebra(使用git help diff获取最新选项及其描述)。例如:

git diff --color-moved=zebra

关于 如何 完成,你可以从该功能的作者在电子邮件中的交流中获取一些理解。


1
有没有一种方法可以配置git,使其默认应用“--color-moved”选项? - Eugen Konkov
2
@EugenKonkov 是的,使用 git config 来设置 diff.colorMoved - Inigo

8
一些功能可以在git gui blame(+文件名)中找到。它显示了文件的行注释,每个注释都指示了该行的创建时间和最后更改时间。对于文件中的代码移动,它将原始文件的提交显示为创建,并将添加到当前文件的提交显示为最后更改。试试看。
我真正想要的是给git log提供一些参数,包括行号范围和文件路径,然后它会显示此代码块的历史记录。如果文档正确,就没有这样的选项。是的,根据Linus的说法,我也认为这样的命令应该是随时可用的。

4
我刚刚第一次看到了gui blame。不错。我开始想可能这就是Linus所说的意思。Git内部并没有存储函数从一个文件移动到另一个文件的信息,但是,鉴于Git实际存储的信息,你可以确定函数是否移动了(如git gui blame或像我在问题中提到的那样通过差异)。如果是这样,这意味着我的最初理解是正确的,即它完全是关于Commits、Trees和Blobs的,Git从不查看文件内容。但这已经足够让您通过分析检测到函数移动了。也许。 - Charlie Flowers
是的,我认为就是这样。Git 后端现在对文件内容一无所知(除了可能将它们作为差异存储为一些大小优化),但前端工具必须完成所有操作。 - Paŭlo Ebermann
似乎只有一个问题...如何按时间顺序浏览历史记录?它有点像置顶帖... - user1115652
@AgentFriday 你可能需要单独安装这个。例如,在Ubuntu上,它可以在git-gui软件包中找到。 - Paŭlo Ebermann

5
Git实际上根本不跟踪重命名。重命名只是删除和添加,仅此而已。任何显示重命名的工具都是从这些历史信息中重建出来的。
因此,跟踪函数重命名只需要事后分析每个提交中所有文件的差异即可。这并不特别困难;现有的重命名跟踪已经处理了“模糊”的重命名,在其中对文件进行了一些更改并将其重命名;这需要查看文件内容。查找函数重命名也可以作为简单的扩展实现。
我不知道基本的Git工具是否实际执行此操作-它们试图保持语言中立,而函数识别则不是语言中立的。

我并不是指“函数重命名”。相反,我想问的是将一个文件的子集文本移动到另一个文件中的情况。 - Charlie Flowers
你是正确的,但你的评论不够清晰,前几个单词会让人觉得你误解了 Q,请编辑一下。关于主题,Git 使用系统 diff,这就是它对此的全部控制能力,它可以“跟踪”函数重命名,但并不特别聪明。基本上只是一行差异,你可以追踪那个东西。 - Tomas Pruzina

2

有一个git diff命令可以显示某些行从foo中消失并出现在bar中。如果这些文件在同一提交中没有其他更改,则更改将很容易被发现。

一款智能的git客户端可以向您展示如何将行从一个文件移动到另一个文件。具备语言意识的IDE可以将此更改与特定功能对应起来。

当文件被重命名时,类似的事情会发生。它只是以不同的名称消失并重新出现,但任何合理的工具都能够注意到它并表示为重命名。


2
有没有现成的客户端可以让人显示函数的历史记录? - William Pursell
1
William:你应该尝试使用“git gui blame path/to/filename.ext”或者“git blame -CCCw path/to/filename.ext”(前者有一个相当好用的GUI,后者包含更好的硬移动和复制诊断)。不幸的是,我认为没有办法将“-CCCw”选项传递给git gui blame。 - Mikko Rantalainen
实际上,使用新版本的git(1.5.3以上),在加载文件后可以通过右键菜单选择“执行完整复制检测”来使用“git gui blame”获取“git blame -CCCw”的结果。(我刚刚查看了位于/usr/share/git-gui/lib/blame.tcl的源文件)。 - Mikko Rantalainen
@underscore_d 你有收到任何警告信息吗?在 git version 2.7.4 下似乎仍然可以工作,而且 git help blame 知道 -C 的用法:“当此选项被使用三次时,该命令还会查找任何提交中其他文件的副本。” - Mikko Rantalainen
@MikkoRantalainen 是的,但这需要将选项作为离散开关多次给出,即“-C -C”或“-C -C -C”。在我的版本中,将多个“C”组合成单个参数不会产生正确的效果。目前,“-C”被记录为接受可选数字参数,因此可能并非始终如此,并且可能导致连续的“C”没有所需的效果(例如,“git”尝试并失败将第二个“C”解释为数字等)。 - underscore_d
显示剩余2条评论

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