Git在修改文件名/移动文件后无法跟踪历史记录

4
问题:我需要更改存储库中整个目录的位置。为此我使用git mv,如果需要,将包含头文件名更改为当前正确的名称。问题在于当我在一次提交中同时执行这两个操作时。在这种情况下,文件历史记录丢失了(git认为这是删除和创建新文件)。 解决方法:如果我将这些操作拆分为单独的提交,则不会出现此问题。 问题重新出现:然而,即使我使用上述解决方案,在与主分支合并时问题还会再次出现。我被强制使用no-ff合并。在这种情况下,主分支的新提交由两个提交的更改组成,但历史记录仍未正确跟踪。 另一个丑陋的解决方法:我可以将这些提交分别提交到主分支。我不能提交无法编译的代码,但如果将其从构建过程中排除,可能是可行的...但这太丑陋了,也是不对的...
我想知道是否有更好的解决方法。

另一个不太优雅的解决方法是:移动整个历史记录中的文件/文件夹。缺点是:1)您会丢失原始目录结构的追踪,2)您需要强制推送。请参考https://dev59.com/HXA75IYBdhLWcg3wuLnD以了解是否适用。 - Fabien Bouleau
@FabienBouleau 这怎么可能?我可以重写所有提交的历史记录,但我无法像在hg或svn中那样强制git正确跟踪文件历史记录...这是一个设计精良的工具。 - guy_from_nowhere
@FabienBouleau 为了避免被误解,我非常感谢您的建议。我只是害怕在git中实现了多少重写历史的可能性(而不是可以帮助协作的功能:P)。我想知道为什么--follow不是默认的日志选项... - guy_from_nowhere
@guy_from_nowhere 没关系,我不会生气的。之所以只能对文件使用“—follow”参数,是因为我认为一方面,“hit”命令只能用于文件,另一方面,如果文件从不同文件夹中移动,你又如何追踪路径的历史呢? - Fabien Bouleau
@guy_from_nowhere:--follow的实现很差,它一次只能处理一个文件名(例如,你不能--follow整个目录中的文件),而且在跟踪合并时效果不佳。这可能就是为什么它不是默认选项的原因。至于历史重写,它实际上是创建了一组新的提交(而不是更改旧的提交):你基本上是将原始存储库复制到一个新的“好像它们一直以那种方式命名”的存储库中,然后放弃原始存储库。 - torek
显示剩余2条评论
2个回答

3
如果你熟悉几乎任何其他版本控制系统(VCS),那么很难理解Git对文件历史记录的处理方式。
事实上,Git没有文件历史记录。在这里,它可能是VCS中独一无二的(尽管我没有使用过许多更晦涩的VCS)。它最接近的表兄弟Mercurial有文件历史记录:Mercurial中添加的每个文件都被分配一个唯一的编号,称为“清单”,并确定文件的身份。如果更改文件的名称或整个目录中的文件,则它们保留其身份,因为此信息存在于清单中。
Git完全放弃了这种概念。Git根本没有文件历史记录。Git只有提交。
每个提交存储源树的完整快照。每个提交还具有一些父提交,通常只有一个。这更像传统的基于提交的VCS:可以跟踪各个提交,或查看文件历史记录。但是由于Git没有文件历史记录,它唯一拥有的就是提交历史记录。
为了实现`git log --follow`和其他有用的项目,Git提供了重命名检测,而不是文件历史记录。Git可以查看任何一个特定的提交,并将该提交与其父提交进行比较,或者对于合并提交,与所有父提交进行比较。当它进行此比较时,它提供了检测通过该提交重命名的文件的选项:在父级中具有一个名称但在子级中具有不同名称的文件。
甚至可以在比较两个任意提交时使用此重命名检测,而这两个提交不仅是父子关系。执行以下操作:
git diff --find-renames $hash1 $hash2

比较两个提交,当“路径为 a/b/c.txt 的文件在 $hash1 中与路径为 d/e/f.log 的文件在 $hash2 中非常相似时,Git 可能会声称该文件已被重命名(然后可能被修改)。但是要记住,Git 仅仅是“合成一种方式将第一个文件转换为第二个文件”。在这两个提交中的实际文件以这种方式永久存储。它们永远不会改变:只要这些提交存在,这两个文件就以这种方式存储在这两个提交中。除非你想让它们相关,否则这两个文件实际上没有任何关系。通过比较它们的相似性,Git 才“发现”了重命名。给 Git 不同的“相似性”标准 - 例如 -M75% 而不是 -M50% - Git 可能会选择一组不同的“足够相似”的文件。
没有任何提交发生过任何变化。它们都冻结在时间中。但是使用不同的“重命名阈值”,“断裂阈值”等,Git 可能会匹配不同的路径名。如果给定 --no-renames,Git 永远不会匹配不同的路径名(尽管它仍将匹配相同名称的文件)。
(这种动态重命名检测在合并时非常重要,因为合并运行了两个 git diff --find-rename 操作,从合并基础提交到正在合并的两个分支提示提交中的每个提交。如果 Git 找到了一次重命名,它就会相信。如果它没有找到一个重命名,它就会认为基础文件已被删除,并且在提示中创建了一个不同的文件。您可以控制重命名阈值,但是至少在 Git 版本(2.15)中,您无法设置断裂或复制阈值。)
这个含义对于合并提交来说不太清楚,因为有多个父级:在父级#1中,文件 child.txt 的名称为p1.txt,而在父级#2中则为p2.txt,这意味着什么?传统的版本控制系统具有唯一的内部编号系统,用于确定文件标识,因此在这里分配了一个明确的含义,但实际上,这个含义并不总是有用的,Linus Torvalds 在这里的选择是完全放弃这个概念,可能部分是对此的反应。

1
你可以将--follow选项设置为git log命令的默认选项:
git config --global log.follow true

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