如何使用Git真正显示重命名文件的日志

183

我对Git相对较新,之前使用过Subversion (SVN)。

我注意到大多数图形化的Git前端和IDE插件似乎无法显示文件历史记录,如果文件已被重命名。当我使用

git log --follow

在命令行中,我可以看到重命名后整个日志。

根据Linus Torvalds替代链接)的说法,--follow开关是“SVN新手”的喜好;真正的Git用户不使用它:

--follow是一个完全的hack,只是为了满足从未了解过像父母或漂亮的修订图表等内容的ex-SVN用户。

它并不是非常基本的东西,但是“--follow”的当前实现确实是一个快速的预处理事情,它被添加到了修订步行逻辑上,而不是任何真正的必要部分。

它的设计初衷就是为了取悦“SVN新手”,而不是作为“真正的git功能”存在。想法是让你摆脱认为重命名在大局中很重要的(破碎的)思维方式。

那么,硬核Git用户如何获取文件的历史记录,当它被重命名时?这样做的“真正”方法是什么?


20
@David Hall:git mv oldfile newfile并没有将重命名操作完全记录下来 - 它只是相当于删除一个文件,再添加一个新文件。 Git只会在之后的每个提交中从树的状态中计算出重命名和复制操作。请注意,这里不会记录重命名操作本身。 - Mark Longair
21
如果您使用git之外的其他工具(例如/bin/mv oldfile newfile)重命名文件,然后执行git add newfile; git rm oldfile,则结果与git mv oldfile newfile的结果无法区分。请注意,这里的翻译保持了原文的意思和用词,并使其更加易懂。 - Mark Longair
1
如果您将文件移动到新的存储库中,这种思想就会崩溃,因为无法移动其整个历史记录可能是一个重大问题。当然,在复杂项目中,文件所带来的真正历史记录有限制。 - Roman Starkov
1
注意:git log --follow 在 git 2.9(2016年6月)有所改进:请参见我的回答 - VonC
2
从v2.15开始,当您进行diff时,您可能想尝试使用--color-moved进行实验。 - Michael
显示剩余6条评论
6个回答

83

我认为Linus的观点背后的原因是,尽管需要保持谨慎,但是Git的高级用户并不关心一个“文件”的历史。你将内容放入Git仓库,是因为整体内容具有意义的历史。

文件重命名是“内容”在路径间移动的一种特殊情况。你可能会有一个函数,在Git用户可以使用“拾取斧头”(例如log -S)功能来追踪它。

其他“路径”变化包括合并和拆分文件;Git 实际上不关心您认为哪个文件重命名,哪个被复制(或重命名并删除)。它只跟踪您的树的完整内容。

Git鼓励“整个树”思维,而许多版本控制系统非常以文件为中心。这就是为什么Git更常提到“路径”而不是“文件名”的原因。


36
Linus的观点是,一个“适当的”图形用户界面应该能够跨文件跟踪代码块,他希望我们现在已经拥有这样的工具。不幸的是,我们仍然没有这种奢侈,因此,“--follow”命令仍然很有用。 - Michael Parker
12
除了 --follow,Git 是否提供其他解决方案呢? - Griwes
25
我认为,“整体树形思维”可以通过将 --follow 设置为默认值来加强。我的意思是,当我想要查看文件中代码的历史记录时,我通常并不关心文件是否被重命名,我只是想查看代码的历史记录,而不考虑重命名情况。因此,在我看来,将 --follow 作为默认选项是有道理的,因为我不关心单个文件;--follow 帮助我忽略了单个文件被重命名的情况,这些情况通常并不重要。 - Eddified
3
如果我决定关注“内容”而不是文件,那么如何打印与当前文件中的内容相关的提交?如果git可以跨不同文件跟踪所有更改并报告日志,我会很高兴的,但我不知道如何做到这一点。 - Ed Avis
3
问题在于文件名实际上具有相关性。例如,用户想知道存在于跨越20年历史的项目中的文件发生了什么。当这样的用户尝试升级到新版本时,他们多年前修改过的文件完全不见了,那么他们如何找出现在需要应用本地更改的位置? - Dread Quixadhal
显示剩余3条评论

42

我遇到了与你完全相同的问题。虽然我无法给出答案,但我相信你可以阅读林纳斯在2005年写的这封电子邮件,它非常相关,可能会为你提供如何处理问题的提示:(链接)

......我声称,除非出于内部原因(例如允许有效的增量),否则任何试图跟踪重命名的 SCM 都是根本错误的,正是因为重命名并不重要。 它们对你没有帮助,而且它们也不是你感兴趣的东西。

重要的是找到“这个从哪里来的”,而 Git 架构确实做得非常好——比其他任何东西都好......

我发现这篇博客文章引用了这篇邮件,你也许会从中找到一个可行的解决方案:

在这封邮件中,Linus 概述了理想的内容追踪系统如何让您找到代码块如何变成当前形状的。您可以从文件中当前代码块开始,向后查找历史记录以找到更改文件的提交。然后,您检查提交的更改,以查看您感兴趣的代码块是否被修改,因为更改文件的提交可能不会触及您感兴趣的代码块,而只会触及文件的其他部分。

当您发现在提交之前该代码块不存在于该文件中时,您需要深入研究提交。您可能会发现它是许多可能情况之一,包括:

  1. 提交确实引入了该代码块。提交的作者是您正在寻找其原始来源的那个酷功能的发明者(或引入错误的罪魁祸首);或者
  2. 在文件中并不存在该代码块,但是该代码块的五个相同副本存在于不同的文件中,在提交后全部消失。提交者通过引入一个单一的帮助函数来消除了重复代码;或者
  3. (作为一个特殊情况) 在提交之前,当前包含您感兴趣的代码块的文件本身并不存在,但另一个具有几乎相同内容的文件存在,并且您感兴趣的代码块以及文件中的所有其他内容都存在于那个文件中。在提交后,它消失了。提交者对该文件进行了重命名并进行了轻微修改。

在 Git 中,Linus 的终极内容跟踪工具还没有完全实现自动化。但是大多数重要的元素已经可用了。

请随时向我们报告你的进展。


感谢您发布这些文章。直到我阅读它们,我才完全掌握了内容历史的概念!我一直以错误的方式思考这个问题! - DavidGamba
那封来自Linus的电子邮件很棒,谢谢你发布它。 - mik01aj
有趣的事实,Git v2.15添加了--color-moved,这是迈向“理想跟踪系统”的一步。我正在尝试使用它来跟踪文件内移动的行,但意外地发现它可以跟踪整个差异中移动的行。 - Michael
3
Linus解释了一个复杂的情况。但是在这里,我们有一个简单的情况:一个文件刚刚被重命名(或移动到另一个目录)。因此应该有一个简单的解决方案。我认为问题出在这个事实上,与Subversion相反,用户不能在提交时指示Git文件来自哪里,并且--follow可能是错误的(例如,如果2个文件具有相同的内容,或者如果除了文件移动之外还有修改)。 - vinc17

17
我注意到大多数图形化的Git前端和IDE插件似乎无法显示已重命名的文件的历史记录。
你会很高兴知道,一些流行的Git用户界面工具现在已经支持这个功能。有数十种Git用户界面工具可供选择,所以我不会列举所有的工具,但是举个例子:
- Sourcetree,在查看文件日志时(右键单击提交中的文件,从下拉菜单中选择“选择日志...”),在左下角有一个“跟踪重命名文件”的复选框。如果一个文件被重命名了多次,你需要跳转到“重命名”提交,并再次重复这个操作。
- TortoiseGit在日志窗口的左下角有一个“跟踪重命名”复选框。
有关Git用户界面工具的更多信息,请参考以下链接。

源代码在重命名一次时运行良好,但在重命名两次时,更改细节对于重命名之前的提交不可用。我在这里报告了错误:https://jira.atlassian.com/browse/SRCTREE-5715 - Intel
gitk 在历史记录中文件重命名两次时也能很好地工作。该命令如下所示:“gitk --follow path/to/file”。 - Intel
太棒了!如果没有你的答案,我永远不会注意到SourceTree中的“跟随重命名的文件”。 - Max MacLeod
SourceTree在2023年对我非常好用。唯一的问题是,如果文件被重命名了两次或更多次,我必须跳转到重命名的提交,才能查看之前的日志,然后对另一个重命名重复这个步骤。但是所有的日志都是可检索的。 - undefined

8

注意:git 2.9(2016年6月)将大大改善“有缺陷”的git log --follow

请参见提交 ca4e3ca(由SZEDER Gábor(szeder于30 Mar 2016提交)。
(由Junio C Hamano -- gitster --合并在提交26effb8中,于2016年4月13日)

diffcore:修复重命名检测期间相同文件的迭代顺序

如果两条路径 'dir/A/file' 和 'dir/B/file' 的内容相同,并且父目录被重命名,例如 'git mv dir other-dir',那么 diffcore 将报告以下准确的重命名:
renamed:    dir/B/file -> other-dir/A/file
renamed:    dir/A/file -> other-dir/B/file

(注意这里的反转:B/file -> A/file,以及A/file -> B/file)
虽然技术上并没有错,但这对用户和基于重命名信息做出决策的git命令也很困惑,例如“git log --follow other-dir/A/file”会跟随“dir/B/file”通过重命名。
这种行为是提交v2.0.0-rc4~8^2~14的副作用(diffcore-rename.c: simplify finding exact renames, 2013-11-14):存储源的哈希表以LIFO顺序返回来自同一桶的条目,即与当前目标匹配的源。因此,迭代首先检查“other-dir/A/file”和“dir/B/file”,在找到相同内容和基本名称后,报告一个确切的重命名。

在 Git 2.31(2021年第一季度)中,diffcore的文件级重命名检测得到了改进。

请查看2020年12月29日提交的提交350410f,以及提交9db2ac5提交b970b4e提交ac14de1提交5c72261提交81c4bf0提交ad8a1be提交00b8ccc提交26a66a6,作者是Elijah Newren (newren)
(由Junio C Hamano -- gitster --于2021年1月25日合并至提交a5ac31b)

diffcore-rename:加速 rename_dst 设置

签名作者:Elijah Newren

register_rename_src() 只是在 rename_src 中引用了传递的一对。

相比之下,add_rename_dst() 对于 rename_dst 做了完全不同的事情。
它没有复制传递的一对,而是复制了传递的第二个 diff_filespec,引用它,然后将 diff_rename_dst.pair 字段设置为 NULL
稍后,当找到匹配时,record_rename_pair() 通过 diff_queue() 分配了一个完整的 diff_filepair,并将其 srcdst 字段指向适当的 diff_filespecs

register_rename_src()rename_src 数据结构以及 add_rename_dst()rename_dst 数据结构之间的这种对比是奇怪的不一致,并且需要比必要更多的内存和工作。
[...] 此补丁将设置时间加速约65%,最终写回输出队列的时间加速约50%,导致重新基于几十个补丁的执行时间总体下降了3.5%。


5

以下是操作步骤:

git log -M --summary | grep rename | grep BASENAME

只需在那里放置基本名称。

如果结果太多,则还可以依次使用grep搜索每个中间目录名称。


谢谢!对于未来的读者:我得到了warning: exhaustive rename detection was skipped due to too many files. \n warning: you may want to set your diff.renameLimit variable to at least 1800 and retry the command.所以我运行了git config --global diff.renameLimit 1800,然后再次运行了命令。 - gawkface
1
此外,这个命令没有告诉提交 ID,所以我在每个 grep 中添加了 --before=20 来查找 grep 上下文中的提交 ID。另外,我发现 grep 的 --line-buffered 选项有助于不必等待管道中的完整结果,从而加快反馈速度! - gawkface

2
在Linux上,我已经验证了SmartGitGitEye可以在跟踪特定文件的历史时跟随重命名。然而,与gitk和GitEye不同,SmartGit显示一个单独的文件视图和存储库视图(其中包含目录结构,但不包含其中包含的文件列表)。

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