我移动了一些目录。
当我合并时,有许多冲突的文件,因为其他开发人员已经提交了他们的更改。 egit Merge Tool和git mergetool
都表示该文件已在本地或远程删除。见图。
如何合并这些更改?
答案 0 :(得分:8)
你永远不需要担心"保留历史"在Git。 Git根本没有 file 历史记录,它只有 commit 历史记录。也就是说,每次提交都指向" (包含哈希ID)其父级 - 或者,对于合并,两者其父级 - 而 是历史记录:commit E
之前是提交D
,而提交D
前面有提交C
,依此类推。只要你有提交,你就有了历史。
也就是说,Git 可以尝试 使用git log --follow
来合成一个特定文件的历史记录。您指定了一个起始提交和一个路径名,并且Git检查commit-by-commit,以查看在将当前提交的父级与当前提交进行比较时是否重命名了该文件。这使用Git的重命名检测来识别提交 L (左)中的文件a/b.txt
是"同一文件"作为提交 R (右)中的文件c/d.txt
。
重命名检测有许多繁琐的旋钮,但在基础层面,基本上就是这样:
a/b.txt
已消失且c/d.txt
为对于检测到的重命名,全新的,为什么,候选人。未配对的文件进入配对队列(一个用于 L ,一个用于 R ),Git会对所有文件的内容进行哈希处理。它已经具有内部Git哈希,因此它首先直接比较所有这些哈希值。如果文件完全未更改,则它在 L 和 R 中具有相同的Git哈希ID(但名称不同),并且可以立即配对-up并从配对队列中删除。
现在精确匹配被取出,Git尝试了长时间的缓慢行动。它需要一个未配对的 L 文件,并计算一个"相似性索引"对于每个 R 文件。如果某些 R 文件足够相似 - 或者几个 - 它需要"最相似的" R 文件并将其与 L 文件配对。如果没有足够相似的文件, L 文件将保持未配对状态(从队列中取出)并被视为"从 L "中删除。最终,未配对的 L 队列中没有文件,并且在未配对的 R 队列中保留了任何文件,这些文件被添加到#34; ( R 中的新内容)。同时,所有配对文件都已重命名。
这意味着:比较(git diff
)提交 L 到 R 时,如果两个文件足够相似,它们就会配对作为重命名。默认相似性指数为50%,因此文件需要50%匹配(无论这意味着 - 相似性指数计算有些不透明),但 exact < / em>匹配对于Git来说更容易,更快。
请注意git log --follow
启用重命名检测(仅在一个目标 R 文件上,因为我们通过日志正在向后工作,比较父级提交只有我们知道的孩子名字的文件。自Git版本2.9起,git diff
和git log -p
现在都自动启用了重命名检测功能。在旧版本中,您必须使用-M
选项设置相似性阈值,或将diff.renames
配置为true
,以便git diff
和git log -p
执行此操作重命名检测。
配对队列也有最大长度。这已经翻了两倍,一次是在Git 1.5.6中,一次是在Git 1.7.5中。您可以自己控制它:它可以配置为diff.renameLimit
和merge.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 提交。通过&#34;分支名称&的定义#34;在Git中,这是该分支的提示,因此这个&#34;只是工作&#34;。但是您可以通过哈希ID指定任何提交,例如。)让我们调用这个合并基础提交 B (对于base)。我们已经知道我们自己的提交是HEAD
,尽管有些事情称之为#34;本地&#34;。让我们调用另一个提交 O (对于其他提交),尽管有些事情称之为&#34; remote&#34; (这很愚蠢:Git中没有任何东西是遥远的!)。
Git实际上是两个 git diff
。一个比较 B 与HEAD,因此对于此特定差异, L 是 B 而 R 是HEAD。根据我们上面看到的规则,Git将检测或未能检测到重命名。然后Git执行另一个git diff
,将 B 与 O 进行比较。 Git将再次根据相同的规则检测或未能检测到重命名。
如果某个文件在 B -vs-HEAD中重命名,Git会照常传播其内容。如果某个文件在 B -vs- O 中重命名,Git会照常传播其内容。如果在HEAD和 O 中将单个 B 文件 F 重命名为两个不同的名称,Git将声明重命名/重命名该文件上的冲突,并在工作树中保留两个名称以供清理。如果它在中只重命名了一个差异 - 它仍然在HEAD或 O 中被称为 F - 那么Git商店工作树中的文件使用重命名的任何一方的新名称。在任何情况下,Git都会像往常一样尝试组合两组更改(来自 B -vs-HEAD和 B -vs- O ) 1
当然,对于Git来检测重命名,文件的内容必须与往常一样。这对于Java文件(有时也是Python)尤其有问题,其中文件名嵌入在import语句中。如果一个模块主要由import语句组成,只有几行代码,则重命名引起的更改将覆盖剩余的文件内容,文件甚至不会达到50%匹配。
有一个解决方案,虽然它有点难看。与git log --follow
的经验法则一样,我们可以先提交 重命名,然后提交内容更改&#34;修复所有导入&#34;作为单独的提交。然后,当我们进行合并时,我们可以两个甚至三个合并:
git checkout ... # whatever branch we plan to merge into
git merge <hash> # merge with everything just before the Great Renaming
由于没有重命名文件,因此这种合并也会像往常一样顺利或差劲。这是结果,以图表的形式。请注意,我们提供给git merge
命令的哈希是提交A
的哈希值,就在执行所有重命名的R
之前:
...--*--o--...--o--M <-- mainline
\ /
o--o--...-A--R--...--o <-- develop, with renames at R
然后:
git merge <hash of R>
由于每个文件的内容完全相同,名称相同,跨另一个R
提交 - 合并基础是提交A
- 此处的效果仅仅是为了获取所有重命名。我们保留HEAD commit M
中的文件内容,但来自R
的名称。此合并应自动成功:
...--*--o--...--o--M--N <-- mainline
\ / /
o--o--...-A--R--...--o <-- develop, with renames at R
现在我们可以git merge develop
继续合并开发分支。
在很多情况下,我们不需要进行合并M
,但无论如何都要做到这一点 如果我们需要进行合并{{} 1}}仅适用于所有重命名。原因是提交N
不起作用:它的导入名称错误。在二分期间必须跳过提交R
。这意味着合并R
同样不起作用,必须在二分期间跳过。让N
出现可能会很好,因为M
实际上可以正常工作。
请注意,如果您执行此操作,则会扭曲/扭曲源代码,只是为了取悦您的版本控制系统。这不是一个好的情况。它可能不那么糟糕比你的其他选择,但不要告诉自己它好。
1 我仍然需要查看重命名/重命名冲突时文件的两个副本会发生什么。由于Git在工作树中都留下了名称,两个名称是否包含相同的合并内容,以及任何冲突标记(如果需要)?也就是说,如果文件名为M
且现在名为base.txt
和head.txt
,那么other.txt
和head.txt
的工作树版本是否始终匹配?