在执行git pull后,暂存区中出现了许多无关的修改。

4
一位同事在本地仓库的 master 上工作了几周后,进行了 git pull。与此同时,在主分支上有许多活动,包括至少10个合并提交(到目前为止我们一直使用合并工作流)。
拉取后,有一些文件存在冲突。但意外的是,他的暂存区中有数百个修改,这些修改都是撤消自公共祖先提交以来对 origin/master 所做更改的结果。例如:

自公共祖先以来的更改

他的分支 (master)

  • A: 添加3行
  • B: 删除2行
  • C: 移除1行
  • G1..G99: 新文件

origin/master

  • C: 添加2行
  • D: 删除10行
  • E: 更改20行
  • ...: 许多其他文件更改
  • N1..N3: 新文件
  • D1..D5: 已删除的文件

执行git pull

暂存区的修改

  • A: 删除了3行
  • B: 添加了2行
  • G1..G99: 新文件
  • 来自origin/master的所有其他更改都消失了。N1-N3缺失,D1-D5回来了。

有冲突

  • C

我已经重现了这个问题,以验证他除了简单的git pull之外没有做任何特殊操作。

不幸的是,他提交了所有这些更改,而我最终需要恢复该提交。但我想了解为什么会发生这种情况,以便我们将来可以避免它。

我已经阅读了some related相关的问题,但没有一个回答了这个问题:当他的分支上对应的文件没有更改时,为什么Git会撤消来自master的更改?我理解如果在拉取后存在冲突,则暂存区中会有修改,但我不理解为什么自共同祖先以来,它们会有效地还原origin/master上的工作。

我们目前解决这个问题的方法是

  • 使用git pull --rebase代替合并
  • 在拉取后检查状态,如果有无法解释的修改,请勿提交

还有一件事我应该提一下 - 合并的origin/master分支有一堆分支,因为人们大多使用合并工作流在master上工作。有些地方的分支深达7层。我能理解Git可能会感到困惑。 - sourcedelica
1个回答

3

编辑(根据问题编辑以获取更多详细信息):似乎有可能是重命名检测出了问题,而且历史记录中可能存在交叉合并,因此Git正在形成一个“虚拟合并基础”。

为了找出原因,请先运行以下命令:

git merge-base --all <hash1> <hash2>

关于两个分支名称 masterorigin/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。然后他们提交所有这些更改,这意味着他们已经更改了每个文件的每一行。)

我已经在我的帖子中添加了澄清。 - sourcedelica
啊哈,我想知道现在是不是重命名检测出了问题。我会添加那个链接和另一个链接... - torek
git diff --name-status --find-renames=50 --diff-filter=R 76ef56fff60b2cbef3f8c787a8d15c522868a914 a6e112cecgit diff --name-status --find-renames=50 --diff-filter=R 76ef56fff60b2cbef3f8c787a8d15c522868a914 eeedb3cdb 都没有返回任何输出。我还尝试了 --find-renames 的值为 25/75/90。 - sourcedelica
嗯...这很有趣,这是一个普通的合并操作,没有重命名,但合并却出了大问题。此时我已经没有“脑海中”的想法了;我需要更仔细地查看存储库本身(以及您可能正在使用的任何特殊配置)。 - torek
好的 - 谢谢你的帮助!我从你的答案中学到了很多。 - sourcedelica
显示剩余2条评论

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