编辑(根据问题编辑以获取更多详细信息):似乎有可能是重命名检测出了问题,而且历史记录中可能存在交叉合并,因此Git正在形成一个“虚拟合并基础”。
为了找出原因,请先运行以下命令:
git merge-base --all <hash1> <hash2>
关于两个分支名称 master
和 origin/master
在错误合并发生时所指向的两个哈希 ID,我们需要测试一下重命名检测是否存在问题以及是否存在多个合并基础。
如果输出的结果显示有 多个 哈希 ID,则表示使用了 recursive
策略来合并合并基础,并将其结果用作新的合并基础。这可能会(在极少数情况下)产生非常混乱的结果。此时,切换到 -s resolve
策略可能会有所帮助。这将选择其中一个合并基础并坚持使用它。(但你无法控制使用哪个合并基础。)请注意,将 merge.conflictStyle
设置为 diff3
也会有时显示虚拟合并基础的效果(但只有有时)。
接下来,无论是否存在多个合并基础,我们都可以检查重命名检测是否存在问题。如果是,则有两件事可能会有所帮助:
- 粗暴的方法:在合并期间完全禁用重命名检测(需要 Git 版本 2.8 或更高版本):添加
-X no-renames
,与 git diff
的拼写相匹配(虽然在那里是 --no-renames
)。
- 更细致的方法:提高检测阈值(默认值为 50%):
-X rename-threshold=100
或 -X find-renames=100
需要精确匹配而不是近似匹配。新的拼写方式 -X find-renames=<n>
与 git diff
选项的拼写方式相匹配,是 Git 版本 2.8 中的新功能,但该选项本身非常古老,自版本 1.7.4 起就存在了。请注意,还允许其他阈值值,尽管 100% 的精确匹配相当显著。
要找出 Git 是否检测到重命名,我们需要获取 合并基础,如果存在多个合并基础,则会有点棘手:我们必须先合并这些合并基础,以获得 Git 将用作新合并基础的真实合并提交。我假设这种情况 不 存在,因为这个过程有点混乱。所以我们将继续查看“合并基础”,使用:
git diff --name-status --find-renames=50 --diff-filter=R <basehash> <hash1>
git diff --name-status --find-renames=50 --diff-filter=R <basehash> <hash2>
<hash1>
和
<hash2>
的值与之前相同。我们告诉Git给我们文件名和状态,然后仅打印状态为
R
(重命名)的文件名。如果Git确实认为某些文件已被重命名,我们将在此处看到它们的旧名称和新名称。Git在合并期间如何组合这些是有点棘手的,但仅存在
R
状态文件就意味着Git将执行此类操作。如果没有文件,则根本不是重命名检测。
(有关
git diff
中重命名检测的详细说明,请参见
此答案。合并代码使用不同的命令行选项,其中一些最近发生了变化。请参见
VonC的答案以及
Disable Git Rename Detection。)
总的来说,在合并时,Git不会选择“任一”方。相反,它会选择“两边”。请记住,整个三向合并事情还有第三面:有“你”的那一面(HEAD),有“他们”的那一面(你正在合并的内容),还有基础。这形成了一个三角形:
o <-- HEAD
...
o
...
...--o--B (base)
...
o
...
o <-- theirs
并且这个合并将它们全部汇聚在一起,形成一个闪亮(我们希望如此)的钻石:
o
/ \
o \
/ \
...--o--B o result
\ /
o /
\ /
o
请参阅
这个答案和
这个更加技术性/详细的答案。
与此同时,至少一些图形用户界面向用户呈现了这个事实。当用户只更改了一个文件时,他们因为有许多文件要更改而感到害怕。他们指示他们的GUI撤销所有其他更改,这意味着抛弃其他用户的工作!然后他们提交了这个更改,你必须还原他们的合并才能重新获取其他用户的工作。
(另一个“触及每个文件”的来源是当用户在他们的设置中启用行尾转换时。他们接收使用LF或CRLF结尾的代码,并将其转换为CRLF或仅LF。然后他们提交所有这些更改,这意味着他们已经更改了每个文件的每一行。)
origin/master
分支有一堆分支,因为人们大多使用合并工作流在master
上工作。有些地方的分支深达7层。我能理解Git可能会感到困惑。 - sourcedelica