Git 真的可以跟踪单个函数从一个文件到另一个文件的移动吗?如果是这样,怎么做?

Posted

技术标签:

【中文标题】Git 真的可以跟踪单个函数从一个文件到另一个文件的移动吗?如果是这样,怎么做?【英文标题】:Can Git really track the movement of a single function from 1 file to another? If so, how? 【发布时间】:2011-06-21 22:55:21 【问题描述】:

我多次遇到这样的说法,如果你将一个函数从一个文件移动到另一个文件,Git 可以跟踪它。例如,this entry 说:“Linus 说,如果你将一个函数从一个文件移动到另一个文件,Git 会告诉你该单个函数在整个移动过程中的历史记录。”

但我对 Git 的一些底层设计有一点了解,但我不明白这怎么可能。所以我想知道......这是一个正确的说法吗?如果是这样,这怎么可能?

我的理解是 Git 将每个文件的内容存储为一个 Blob,每个 Blob 都有一个全局唯一标识,该标识来自其内容和大小的 SHA 哈希值。然后 Git 将文件夹表示为树。任何文件名信息都属于树,而不属于 Blob,因此例如文件重命名显示为对树的更改,而不是对 Blob。

因此,如果我有一个名为“foo”的文件,其中包含 20 个函数,以及一个名为“bar”的文件,其中包含 5 个函数,我将其中一个函数从 foo 移动到 bar(导致 19 和 6,分别),Git 如何检测到我将该函数从一个文件移动到另一个文件?

据我了解,这将导致 2 个新 blob 存在(一个用于修改后的 foo,一个用于修改后的 bar)。我意识到可以计算一个差异来表明该函数已从一个文件移动到另一个文件。但我不知道函数的历史如何可能与 bar 而不是 foo 相关联(无论如何都不会自动)。

如果 Git 要真正查看单个文件的内部,并为每个函数计算一个 blob(这将是疯狂/不可行的,因为您必须知道如何解析任何可能的语言),然后我可以看到这是怎么可能的。

那么……这个说法正确与否?如果它是正确的,那我的理解中缺少什么?

【问题讨论】:

我不认为它跟踪“函数”而是“代码块”——所以如果你有一个 30 行的函数并将它分成两个 15 行的函数,它会跟踪就像你移动整个函数一样。如果我错了,请有人纠正我。 我的理解(这很可能是错误的,这就是我要问的原因)是每个文件最多对应一个 Blob。因此,在同一个文件中将一个 func 拆分为 2 个较小的 func 只会导致旧的 Blob 被新的 Blob 替换。如果这是正确的,那么它就不会真正跟踪“代码块”,因为它从不查看文件内部。也就是说,它的最小粒度是一个完整的文件。 将 GIT 与语言解析器集成的有趣想法。我想我需要这个功能才能让 Delphi 语言能够将单个 *.pas 拆分为多个 *.pas 文件,其中每个 pas 文件包含一个对象和实现左右。然后希望通过对原始文件的更改来更新这些拆分的文件。所以这可以用作“隐形跟踪”;)可以从本地重组工作中受益,以防主维护者不想重组。 如果你只是将文件分成两个(或几个块),那么possible 会欺骗两个(+)分支中的移动指针指向同一个旧文件,所以当你合并这两个分支你会得到相同的文件“重命名两次”(或更多次),这意味着两个+具有相同祖先的文件的移动。但是,正如您所观察到的,仅仅为了将一点 sn-p 从一个大文件移动到另一个大文件,这个技巧是行不通的。只有基于 AST(通常是特定于语言)的工具才能高精度地跟踪重构。 另外,这是真的,因为下面的一些答案说技术上没有父文件指针,但是如果你在同一个提交中重命名和更改文件时查看 gitk,你会看到类似“相似性index 95% rename from src/foo.txt rename to src/bar.txt"。这来自git-diff-index 后端。因此它通过(高)文本相似性跟踪移动。基本上,为了帮助 git 跟踪重命名,除了文件重命名之外,您需要进行尽可能少的更改的中间提交。 【参考方案1】:

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

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

尝试使用git blame -C 在测试存储库中自己尝试一下,您会发现刚刚移动的代码块源自它所属的原始文件。

来自git help blame 手册页:

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

【讨论】:

作为测试,我创建了一个包含三个文件的 repo,并在 file1 中添加了一行然后提交。然后我将该行移至file2,并再次提交。然后到file3,并提交。 git blame -C10 file3 然后显示了该行添加到 file1 的第一个提交,但我真的很想看到移动该行的 最近一次提交(即,将该行移动到 file2 的提交。)有没有办法做到这一点?我通过使用git log -S'my interesting line' 获得了一些有用的信息,但仍然不是我想要的。 @Johann 似乎普通的git blame 适合这个。 @andrybak 已经 4 年了,所以我不记得我真正想要完成的是什么。但是git blame 只会显示该行的最新更改(无论是否移动),我的评论要求“最近提交 移动该行”(大概是在更多提交之后已更改线路)。 -CC-CCC 似乎不起作用...在git version 2.15.0.rc0 上,我需要多次单独传递隔离的-C 开关,以使其具有记录的效果.文档 kinda 至少暗示了这一点。然而,这个答案和其他 cmets 表明这在过去是有效的。嗯。 从 Git 2.15 开始,我认为有 a better way。【参考方案2】:

截至Git 2.15, git diff now supports 使用--color-moved 选项检测移动的线。它适用于跨文件移动。

显然,它适用于彩色终端输出。据我所知,没有选项可以以纯文本补丁格式指示移动,但这是有道理的。

对于默认行为,请尝试

git diff --color-moved

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

git diff --color-moved=zebra

至于是怎么做的,大家可以从this email exchange by the author of the functionality了解一下。

【讨论】:

有没有办法配置git它默认应用--color-moved选项? @EugenKonkov 是的,使用git config 设置diff.colorMoved【参考方案3】:

这个功能的一部分在git gui blame(+文件名)中。它显示文件行的注释,每行都指示文件的创建时间和最后更改时间。对于跨文件的代码移动,它将原始文件的提交显示为创建,并将其添加到当前文件的提交显示为last change。试试看。

我真正想要的是给git log 作为一些参数,除了文件路径之外还有一个行号范围,然后它会显示这个代码块的历史。如果文档正确,则没有这样的选项。是的,从 Linus 的声明中我也认为这样的命令应该是现成的。

【讨论】:

我才第一次看到gui责备。好的。我开始认为也许这就是 Linus 的意思。并不是说 Git 在内部存储了说明函数从一个文件移动到另一个文件的信息,但是,鉴于 Git 确实 存储的信息,您可以确定该函数移动了(例如git gui blame 确实如此,或者通过我在问题中提到的差异)。如果是这样,这意味着我最初的理解是正确的,即所有关于提交、树和 Blob,并且 Git 从不查看文件内部。但这足以让您通过分析检测函数移动。也许吧。 是的,我想就是这样。 git 后端现在对文件内容不做任何事情(除了可能将它们存储为 diff 的大小优化),但前端工具必须做所有事情。 似乎只是有一个问题......我如何按时间顺序浏览历史?这有点顶贴... @AgentFriday 您可能需要单独安装that。例如,在 Ubuntu 上,它位于 git-gui 包中。【参考方案4】:

git 根本不跟踪重命名。重命名只是删除和添加,仅此而已。任何显示重命名的工具都会根据这些历史信息重建它们。

因此,跟踪函数重命名只是事后分析每次提交中所有文件的差异的简单问题。没有什么特别不可能的。现有的重命名跟踪已经处理了“模糊”重命名,其中对文件进行了一些更改以及重命名;这需要查看文件的内容。查找函数重命名也是一个简单的扩展。

我不知道基本的 git 工具是否真的这样做了——它们试图保持语言中立,而函数识别在很大程度上不是语言中立的。

【讨论】:

我指的不是“函数重命名”。相反,我问的是将一个文件的文本子集从该文件移出另一个文件的情况。 你是对的,但你的评论不清楚,前几句话会暗示(我)你误解了 Q,请编辑它或其他什么。在主题上,git 使用(系统?)差异,这就是它拥有的全部力量,它可以“跟踪”功能重命名,但它并不是特别聪明。它基本上只是一行差异,你可以跟踪那个东西。【参考方案5】:

git diff 将显示某些行从 foo 消失并重新出现在 bar。如果在同一次提交中这些文件没有其他更改,那么更改将很容易被发现。

一个聪明的git 客户将能够向您展示行如何从一个文件移动到另一个文件。具有语言感知能力的 IDE 将能够将此更改与特定功能相对应。

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

【讨论】:

是否存在允许人们显示函数历史记录的客户端? William:你应该尝试“git gui blame path/to/filename.ext”或“git blame -CCCw path/to/filename.ext”(前者有一个非常可用的 GUI,而后者包括更好的硬移动和复制诊断)。不幸的是,我认为没有办法将“-CCCw”选项传递给 git gui blame。 实际上“git gui blame”可用于获取“git blame -CCCw”的结果,方法是使用比1.5.3更新的git并从鼠标右键上下文菜单中选择“Do full copy detection”加载文件后(我刚刚检查了 /usr/share/git-gui/lib/blame.tcl 的源文件)。 @MikkoRantalainen -CC-CCC 曾经工作过吗?他们现在似乎没有(git version 2.15.0.rc0) @underscore_d 您是否收到某种警告消息?似乎仍然可以与git version 2.7.4git help blame 一起使用知道-C:“当这个选项被给出三次时,该命令还会在任何提交中查找来自其他文件的副本。”

以上是关于Git 真的可以跟踪单个函数从一个文件到另一个文件的移动吗?如果是这样,怎么做?的主要内容,如果未能解决你的问题,请参考以下文章

Git合并指定文件到另一个分支

Git合并指定文件到另一个分支

如何以透明的方式从另一个 git 分支获取目录的副本?

如何将文件夹从 git repo 链接到另一个 repo?

如何使用 Git / IntelliJ 将整个文件从一个分支移动/复制到另一个分支? [复制]

忽略已提交到 Git 存储库的文件 [重复]