我该如何在Git中合并移动了位置的文件所做的更改?

6
我移动了一些目录。
当我合并时,由于其他开发人员提交了他们的更改,所以有许多冲突的文件。无论是使用 egit 合并工具还是 git mergetool,都会显示文件在本地或远程已被删除。请参见图像。
我该如何合并这些更改?

enter image description here


这很混乱,因为有一种说法是其他开发人员曾经在某些文件上工作,而另一种说法是这些文件被删除(移动)了。理想情况下,在计划移动或重命名文件时不应同时对文件进行更改。 - Tim Biegeleisen
有多少个文件存在冲突? - Tim Biegeleisen
我为了使用特定的Maven设置,移动了我的整个源代码目录。 - Joshua Fox
这可能会变得很糟糕。你可以从它们的原始位置(您的合作者编辑它们的地方)复制冲突文件的内容并覆盖新位置。更好的方法是放弃您的更改,拉取最新版本,然后移动文件夹,告诉其他人停止工作直到您完成。 - Tim Biegeleisen
谢谢。我知道Git不能追踪文件重命名,但它确实有一些内容跟踪的功能。这个目录移动的工作量很大,需要进一步开发工具和一些测试;我不能要求其他开发人员在此期间停止工作。是否有某种特殊的移动文件方式能够保留历史记录?https://dev59.com/_XNA5IYBdhLWcg3wF5uO - Joshua Fox
显示剩余3条评论
1个回答

13

文件历史和重命名检测

在 Git 中,“保留历史记录”不是必要的。Git 没有文件历史,只有提交历史。也就是说,每个提交“指向”(包含哈希 ID)它的父提交,或者对于一个合并提交,指向它的两个父提交,这就是历史记录:提交 E 之前是提交 D,提交 D 之前是提交 C,以此类推。只要你有提交,就有了历史记录。

尽管如此,Git 可以尝试合成特定文件的历史记录,使用 git log --follow 命令。你需要指定一个起始提交和文件路径,Git 逐个提交进行比较,以确定该文件是否已被重命名。这使用了 Git 的重命名检测功能,来标识提交 L 中的文件 a/b.txt与提交 R 中的文件 c/d.txt是“同一文件”。

重命名检测有很多微妙的选项,但根本上它是这样工作的:

  • Git 查看提交 L 中所有的文件名。
  • Git 查看提交 R 中所有的文件名。
  • 如果有文件名从 L 消失并出现在 R 中,例如 a/b.txt 消失了而 c/d.txt 是全新的,那么这是一个检测到的重命名候选者
  • 现在有了候选者(未成对的 L 文件和未成对的 R 文件),Git 比较这些未配对文件的内容

未成对的文件进入一个配对队列(一个用于 L,一个用于 R),Git 对所有文件的内容进行哈希处理。它已经具有内部 Git 哈希值,因此首先直接比较它们。如果一个文件完全没有更改,则它在 LR 中具有相同的 Git 哈希 ID(但名称不同),可以立即配对并从配对队列中删除。

现在,在不精确匹配之后,Git会进行长时间的耗时分析。它会对每个R文件计算一个“相似度指数”,如果某个R文件足够相似,或者有几个文件足够相似,Git就会选择最相似的R文件并将其与L文件配对。如果没有文件足够相似,L文件将保持未配对状态(从队列中移除)并被视为“从L中删除”。最终,未配对的L队列中没有文件,而未配对的R队列中剩余的文件则被视为“添加”(在R中新增)。与此同时,所有已配对的文件都已被重命名

这意味着:当比较(git diff)提交LR时,如果两个文件足够相似,则它们将被配对重命名。默认的相似性指数为50%,因此文件需要达到50%的匹配程度(不管这是什么意思 - 相似性指数计算方法有些难以理解),但是完全匹配对于Git来说更加简单快速。

请注意,git log --follow启用了重命名检测(仅针对一个目标R文件,因为我们在日志中向后工作,将父提交与仅知道子提交中的某个文件进行比较)。自Git版本2.9以来,git diffgit log -p现已自动开启重命名检测。在旧版中,您必须使用-M选项设置相似性阈值,或配置diff.renamestrue,才能使git diffgit log -p执行重命名检测。

还有一种配对队列的最大长度。它已经在Git 1.5.6和Git 1.7.5中分别加倍。您可以自行控制:它可以配置为diff.renameLimitmerge.renameLimit。当前限制为400和1000。(如果将这些设置为零,则Git将使用其自己的内部最大值,这可能会耗费大量CPU时间 - 这就是为什么首先存在这两个限制的原因。如果只设置diff.renameLimit而不设置merge.renameLimit,则git merge将使用您的差异设置。)

这导致了一个经验法则适用于git log --follow如果可能的话,在重命名某个文件或一组文件时,单独提交重命名步骤,不要更改任何文件内容。 如果可能的话,保持重命名文件数量相对较小:例如在400以下。您可以分多个步骤提交更多的重命名,每次400个。但请记住,您正在权衡git log --follow能力和速度与将历史记录混乱化的无意义提交之间的关系:如果您需要重命名50000个文件,也许您应该这样做。

但是这会影响合并吗?好吧,git merge,就像git log --follow一样,总是打开重命名检测。 但是哪个提交是L,哪个提交或提交是R

合并和重命名检测

无论何时运行:

git merge <commit-specifier>

Git需要找到你当前(HEAD)提交和指定的其他提交之间的合并基础(merge base)。 (通常只是git merge <branchname>。通过将分支名称解析为指向的提交来选择该其他分支的tip提交。根据Git中“分支名称”的定义,这是该分支的尖端提交,因此这“只是起作用”。“ 但是,您可以通过哈希ID指定任何提交(例如)。让我们称此合并基础提交为B(基础)。虽然有些东西称之为“本地”,但我们已经知道自己提交是HEAD。让我们将另一个提交称为O(其他),尽管有些东西称之为“远程”(这很傻:Git中没有远程!)。

然后,Git实际上执行了两个git diff 。其中一个比较B vs HEAD,因此对于这个特定的diff,LBR是HEAD。 Git将根据我们上面看到的规则检测或无法检测重命名。然后Git进行另一个git diff ,它将BO进行比较。 Git将再次根据相同的规则检测或无法检测重命名。

如果在B-vs-HEAD中重命名了某个文件,则Git通常会像往常一样比较其内容。如果在B-vs-O中重命名某个文件,则Git会像往常一样比较其内容。如果单个的B文件F在HEAD和O中被重命名为两个不同的名称,则Git会在该文件上声明一个重命名/重命名冲突,并留下两个 名称在工作树中供您清理。如果在仅一个diff中重命名它,即在HEAD或O中仍称为F ,那么Git将使用重命名它的任何一侧的新名称将文件存储在工作树中。无论如何,Git都会像往常一样尝试合并两组更改(从B-vs-HEAD和B-vs-O )。1

当然,对于Git来检测重命名,文件的内容必须足够相似,就像以往一样。这对于Java文件(有时也适用于Python)特别有问题,其中文件名嵌入了导入语句中。如果一个模块主要由导入语句组成,只有很少的自己的代码行,则重命名引起的更改将压倒其余文件内容,并且文件甚至不会匹配50%。

有一个解决方案,虽然有点丑陋。与git log --follow 的经验法则一样,我们可以先提交重命名,然后将“修复所有导入”的内容更改作为单独的提交进行提交。然后,当我们进行合并时,我们可以执行两个或甚至三个合并:

git checkout ...  # whatever branch we plan to merge into
git merge <hash>  # merge with everything just before the Great Renaming

由于没有重命名文件,这次合并将像往常一样进行得好或不好。以下是以图形方式呈现的结果。请注意,我们提供给git merge命令的哈希值是 A 提交的哈希值,在执行所有重命名操作之前就已经提交了:

...--*--o--...--o--M    <-- mainline
      \           /
       o--o--...-A--R--...--o   <-- develop, with renames at R

然后:

git merge <hash of R>

由于每个文件的内容在名称上与其他R提交完全相同——合并基础是提交A——这里的效果仅仅是捡起所有重命名的部分。我们保留来自HEAD提交M的文件内容,但使用了R的名称。此次合并应该会自动成功:

...--*--o--...--o--M--N    <-- mainline
      \           /  /
       o--o--...-A--R--...--o   <-- develop, with renames at R

现在我们可以使用git merge develop来合并开发分支。

在许多情况下,我们不需要进行合并M,但是如果我们需要为了所有的重命名进行合并N,那么也许这样做并不是一个坏主意。原因是提交R无效的:它有错误的导入名称。提交R必须在二分期间跳过。这意味着合并N同样是无效的,并且必须在二分期间跳过。保留M可能是个好主意,因为M实际上可以工作。

请注意,如果您这样做,您就是通过扭曲/扭转您的源代码来取悦您的版本控制系统。这不是一个好的情况。它可能比其他选择更少糟糕,但不要告诉自己这是好的


1我仍需要看看当存在重命名/重命名冲突时文件的两个副本会发生什么。由于Git将两个名称留在工作树中,两个名称是否都包含相同的合并内容(如果需要,还包括任何冲突标记)?也就是说,如果文件命名为base.txt,现在命名为head.txtother.txt,那么head.txtother.txt的工作树版本是否总是匹配的?


谢谢!非常详细。我需要移动我的 src 目录,但不更改内容(用于基于Maven的工具的文件系统结构)。我可以使用Eclipse移动文件,然后在Eclipse中提交吗?似乎即使对于数百个文件,每个文件都会找到一个相同的对。还是应该有一个脚本来执行 git mv 和每个单独文件的提交。我将有数百个提交,但保证每个文件都成对出现。在此之后,预移动分支上的开发人员将编辑文件,我也将编辑文件,但我将能够轻松合并。这有意义吗? - Joshua Fox
我对Eclipse一无所知。只要它能运行Git命令,你应该会得到相同的行为,但从其他评论和问题中可以看出,eGit是其自己的Git Java实现,因此可能有其自己不同的怪癖。例如,它可能会进行自己的配对,并具有不同的限制。我再次指出,为了让版本控制系统满意而创建许多提交并不好:如果只需要一个这样的提交就能解决所有问题,那就更好了。 - torek

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