Git和文件重命名和替换

Posted

技术标签:

【中文标题】Git和文件重命名和替换【英文标题】:Git and file renaming and replacing 【发布时间】:2014-11-06 07:46:03 【问题描述】:

使用 git 重命名通常没有问题,但我遇到了一个非常困难的问题,我正在努力解决。

由于各种原因,我有一个文件dir1/file。由于一些很久以前的决定,它在完全错误的地方,需要转移到dir2/file

但是有很多代码需要更改,并且由于各种原因,我们不得不将文件在新位置和旧位置保留一段时间。

因此,自然(ish)的方法是这样做:

git mv dir1/file dir2/file
git commit -a

到目前为止一切顺利:

> git diff master --name-status --find-renames
R100 dir1/file dir2/file

那么我们就这样做

ln -s ../dir2/file dir1/file
git commit -a

但是会发生这种情况

> git diff master --name-status --find-renames
A    dir2/file
T    dir1/file

如果有人在 master 上更改 dir1/file 并且我尝试将其拉出,我会被告知与 dir1/file1 存在合并冲突,并且 dir2/file1 保持不变。我从阅读 git 跟踪内容的其他帖子中想到,但它似乎在跟踪文件名和内容。并且完全忽略了内容已经移动的事实。

那么到底如何让 git 识别出我实际上重命名了一个文件,然后添加了一个恰好与旧文件同名的新文件?

注意:我不想将其作为多次推送来执行。有几个像这样的文件受到影响,并且有人同时对其中一个文件进行更改的可能性非常高,并且不能保证他们能够进行拉取以获取重命名,然后拉取以获取软链接.

加法示例。我正在从 python 模块 __init__.py 中删除一个函数,它不应该存在,__init__.py 应该是空的。这也没有被发现为重命名。即使新文件的内容与原始 __init__.py 的内容有 99% 的相同,新的 __init__.py 的内容与旧的内容也有 0% 的相同。在我添加同名文件之前一切都很好。

【问题讨论】:

【参考方案1】:

事实上,Git 会跟踪内容而不是——或者更确切地说,我们应该说“除了”——名称。 diff 出错是因为git diff(必然)尝试映射名称并比较两个单独提交的内容(或者一个提交和当前工作目录,或者一个提交和当前索引等,但这些只是变体“比较两个提交”的主题)。

更具体地说,git diff比较树1T1T2,它默认情况下,重命名的唯一候选是那些在T1中存在某些文件名但在T2中不存在的文件名, T2 中存在其他一些(不同的)文件名,但 T1 中不存在。

因此,当您进行第一次提交时,您有两个提交——我们称之为 A 和 B——有两棵树,其中 dir1/file1 从 A“消失”,dir2/file2 出现在 B 中。这是重命名的候选者-detection,并且由于文件内容 100% 相同,git 很容易发现重命名并为您提供 R100 diff 输出。

当您进行第二次提交时,您添加了带有第三棵树的提交 C。比较 B 和 C 效果很好:dir2/file 出现在两者中,新的符号链接 dir1/file 只出现在 C 中,并且这对的差异输出也很好。比较 A 和 C 时出现问题:现在 dir1/file1 出现在两者中,而 dir2/file2 仅出现在 C 中,git diff 没有意识到存在重命名候选。

有一个标志,--find-copies-harder——或者您可以多次指定-C——这(毫不奇怪)使复制/重命名检测代码更加工作。在这种情况下,git 将考虑“看起来未更改”的文件(在两棵树中具有相同的名称)可能已被复制或重命名为另一个“看起来是新的”文件(存在于第二棵树中但不存在于第一棵树中)的可能性。默认情况下不启用此功能,因为完全通用版本的计算量非常大。


不幸的是,在计算git merge 的差异集时,无法控制使用的差异选项。合并命令设置一些默认值(-M50% 等)并进行一些差异,并且不允许您设置 --find-copies-harder。因此,即使这适用于手动 git diff,它也无法解决您的合并冲突。

请注意,当您进行合并时,2 git 只计算两组差异:从合并基础3 到当前的 HEAD,以及从合并基础到合并提交(git 合并提交,而不是分支:当该提交是分支的尖端时,结果合并该分支的事实是一种“故意巧合”)。因此,可以将重命名作为一次提交,将符号链接作为第二次提交,但是要让git merge“看到”重命名,您还必须执行两个单独的git merges。这不是特别令人愉快,但要解决这个问题,你必须让 git 的 diff 机器更智能,这样它至少可以弄清楚文件类型更改会增加找到重命名的机会,如果它“发现复制/重命名有点困难”。

(请注意,将其添加到 diff 机制将解决这两个问题 - git diff 看不到重命名,而 git merge 看不到重命名 - 一次全部解决。)


1这里的“树”是指完整的文件树,而不是 git 的 tree 对象。

2更具体地说,这是双父合并的情况。章鱼合并的处理方式不同。我还没有深入了解章鱼合并的内脏,也不能再多说什么了。

3merge-base 取决于要合并的两个(或多个)commit,如果有多个 merge-base,则使用默认 (recursive) 策略使事情复杂化候选人,git 计算一个“虚拟合并基础”,它不一定与任何实际提交相同。细节不是我可以在这里正确解释的:我知道一般的想法,但不知道 git 中的细节,无论如何它很少重要并且与您的问题没有直接关系。如果您想阅读更多内容,有一个相当不错的示例 here,尽管该示例使用了一些类似 Clearcase 的术语。

【讨论】:

即使在 7 年后,这也是一个深入且非常有用的答案。谢谢!

以上是关于Git和文件重命名和替换的主要内容,如果未能解决你的问题,请参考以下文章

git提交重命名文件

git重命名文件和文件夹

git diff 重命名/移动和修改的文件,但跳过重命名/移动和相同的文件

重命名文件时替换和添加前导零

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

git:重命名文件并更改文件内容