Git和文件重命名和替换

5

我通常没有使用git重命名的问题,但我遇到了一个非常棘手的问题需要解决。

由于某些早期的决定,我们有一个文件dir1/file,它完全在错误的位置,并且需要移动到dir2/file

然而,有很多代码需要更改,出于各种原因,我们必须将文件保留在新位置和旧位置一段时间。

因此,自然(或近似自然)的方法是这样做:

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

如果有人在主分支上更改了dir1/file,当我尝试拉取时会被告知与dir1/file1存在合并冲突,而dir2/file1则保持不变。从其他帖子中的阅读中,我认为Git跟踪内容,但它似乎也跟踪文件名以及内容。完全忽略了内容已经移动的事实。
那么,我该如何让Git认识到我已经重命名了一个文件,并添加了一个新文件,这个新文件恰好与旧文件同名?
注意:我不想进行多次推送。有几个受影响的文件都是这种情况,同时有人在并行地对其中之一进行更改的可能性很高,而且不能保证他们能够进行拉取以获取重命名,然后进行拉取以获取软链接。
附加示例。我正在从Python模块__init__.py中删除一个函数,这个函数本来就不应该在里面,__init__.py应该是空的。这也没有被识别为重命名。即使新文件的内容与原始__init__.py的内容99%相同,新__init__.py的内容与旧内容的0%相同。一切都很好,直到我添加了一个同名文件。
1个回答

11

事实上,Git跟踪的是内容,而不是名称,或者我们应该说“除了名称之外还跟踪内容”。由于git diff(必要时)尝试映射名称并比较两个不同提交的内容(或一个提交和当前工作目录、一个提交和当前索引等等,但这些只是“比较两个提交”的变体),所以差异出现了问题。

更具体地说,当git diff比较树1T1T2时,默认情况下假定重命名的唯一候选项是那些在T1中存在某些文件名但在T2中不存在,而在T2中存在另一个(不同的)文件名,而不存在于T1中。

因此,当您进行第一次提交时,您有两个提交——让我们称它们为A和B,其中包含dir1/file1从A“消失”,而dir2/file2在B中出现。这是重命名检测的候选项,因为文件内容是完全相同的,Git很容易就能发现重命名并提供R100的差异输出。

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

有一个标志,--find-copies-harder 或者你可以多次指定 -C ,它会使得复制/重命名检测代码更加努力地工作。在这种情况下,Git 将考虑这样一种可能性:一个文件在两个树中“看起来没有变化”(在两个树中都有相同的名称),但实际上可能已经被复制或重命名为另一个“看起来是新的”文件(存在于第二个树中但不存在于第一个树中)。这不是默认启用的,因为完全通用的版本需要非常消耗计算资源。
不幸的是,在计算 git merge 的 diff 集时,无法控制所使用的 diff 选项。合并命令会设置一些默认值(-M50% 等),执行几个 diffs,并且不允许你设置 --find-copies-harder。因此,即使手动执行 git diff 可行,也无法解决合并冲突。
请注意,当你执行合并操作时2,Git 只会计算两组 diffs:从合并基础3到当前的 HEAD,以及从合并基础到合并的提交(Git 合并提交而不是分支:当该提交是分支的末尾时,结果合并该分支是一种“有意的巧合”)。因此,你可以将重命名作为一个提交,符号链接作为第二个提交,但要使 git merge “看到” 重命名,你还必须执行两个单独的 git merge。这并不是特别令人愉快,但如果要解决这个问题,你需要让 Git 的 diff 机制更加智能化,以便它能够至少找到文件类型更改会增大重命名发现的几率。
(请注意,将其添加到 diff 机制中将同时解决这两个问题——即 git diff 没有看到重命名和 git merge 没有看到重命名。)

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

2更具体地说,这适用于两个父提交的合并。章鱼合并的处理方式不同。我没有深入研究章鱼合并的内部情况,对此不能再多做解释。

3合并基点取决于要合并的两个(或多个)提交。此外,默认情况下(使用recursive策略),如果有多个合并基点候选项,则Git会计算一个“虚拟合并基点”,它不一定与任何实际提交相同。我无法在此妥善地解释细节:我知道大概的想法,但不知道Git内部的具体问题,而且在任何情况下,这很少重要,与您的问题没有直接关系。如果您想了解更多信息,请参阅此处的一个相当不错的例子,尽管该示例使用了一些类似Clearcase的术语。


即使7年后,这仍然是一篇深入而极其有用的答案。谢谢! - Rezkin

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