如何解决 git 我想保留更改并拒绝在最新分支中删除文件的冲突

how to resolve git conflict where i want to keep a change and reject the file deletion in latest branch

CONFLICT (modify/delete): src/a.js在aa1e4d中删除,在HEAD中修改。 src/a.js 的版本 HEAD 留在树中。 我想保留 HEAD 中的内容并拒绝 aa1e4d

中的删除

[Git said]

CONFLICT (modify/delete): src/a.js deleted in aa1e4d and modified
in HEAD. Version HEAD of src/a.js left in tree.

I want to keep what is there in the HEAD and reject the deletion in aa1e4d

Git 已经做到了——或者更准确地说,在您的 工作树 中做到了。那么,您现在需要做的就是告诉 Git 这实际上是正确的分辨率:

git add src/a.js

一旦您告诉 Git 所有必要的解决方案,您就可以完成您正在执行的任何操作(git mergegit rebase 或任何可能的操作 — 所有这些调用 Git 的合并机制)。在 Git、运行 git merge --continuegit rebase --continue 的现代版本中,或者无论你用 --continue 做什么来告诉 Git 继续进一步暂停操作。

了解正在发生的事情

了解您在这里做什么很重要:您将需要此类信息用于将来的合并、变基等。

在 Git 中,合并操作——我喜欢称其为动词合并——是各种Git 命令采用某些输入文件的 三个 版本,并使用这三个版本为每个文件提供一个版本。这种合并的一个常见来源,也是一个易于理解的来源——至少 更容易 ——是 git merge 命令本身,通常是 运行通过 git pull.

合并有三个输入

这三个输入是什么?好吧,请记住 Git 实际上就是 提交 。这与分支无关,尽管分支名称可以帮助您 find 提交。它甚至与文件无关,尽管提交包含文件。 Git 是关于提交的。所以这三个输入是三个特定的提交。

当我们使用 git merge 时,我们自己选择其中两个提交。让我们绘制一对分支,每个分支都有一些对该特定分支私有的提交和一些共同的提交(在两个分支之间共享)。我们将把较旧的提交放在左边,较新的提交放在右边,而不是实际的提交哈希 ID(如 aa1e4d),我们将只使用一个大写字母代表每个提交:

          I--J   <-- branch1 (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

在这里,我们在分支 branch1 上,我们将 运行 git merge branch2 合并提交 L 与提交 J

如果一切顺利,最终我们会产生一个新的合并提交。这使用相同的词 merge,但作为形容词修饰词 commit。 Git 还调用结果 a merge,使用单词 merge 作为名词。结果看起来像这样,假设我们得到一个结果并保留它:

          I--J
         /    \
...--G--H      M   <-- branch1 (HEAD)
         \    /
          K--L   <-- branch2

这个新的合并提交 M 仅在分支 branch1 上,但是有两个 parents(而不是通常单个 parent) 三个输入提交中的两个,即 J——我们开始时使用的提交——和 L,我们告诉 Git 合并的提交。

与任何提交一样,新的合并 M 将保存 每个 文件的完整快照 Git 知道。 Git 知道的文件将是来自三个输入提交的文件。这三个输入提交每个都包含每个文件的完整快照,Git在您或任何人进行这些提交时知道这些文件。

我一直提到 三个 提交。这里的三个提交是什么?就输入而言,我们只显示了 两个 提交:J,我们当前或来自 branch1HEAD 提交,以及 Lbranch2 上的最后一次提交,因为 branch2 现在是这样。 第三次 提交是最佳共同祖先

请注意,如果我们从提交 J 开始并向后工作 ,我们将找到提交 I,然后提交 H,然后提交 G,等等。同时,如果我们从提交 L 开始并向后工作,我们将找到提交 K,然后是提交 H,然后是提交 G,依此类推。我们的两条路径在提交 H 处汇合,这两个分支从一个共同的祖先分支出来。

这就是 Git 的工作原理。这就像一个大家谱或 family-tree:提交 J 有提交 I 作为它的(一个,单个)parent。提交 I 将提交 H 作为其 parent。提交 H 将提交 G 作为其 parent。我们工作向后,通过时间倒退,从最近的提交开始作为我们的当前或HEAD 提交。大多数提交只有一个 parent,但偶尔,我们会遇到一个 合并提交 ,如 M,其中有两个 parent。 (我们现在不会担心这个,但是当我们在向后工作时确实遇到合并时,我们通常必须遵循 both parents。)

无论如何,看一下图表,很明显 GH 都可以用作共享的共同祖先。在这种情况下,best 被定义为提交 H。对于像这样的简单图表,最好的是显而易见的:它是最新的,并且 HG 更新,所以这是最常见的共享提交。 技术术语因为这是合并基础.

合并的工作原理

要执行合并,git merge 将:

  • 将合并基础提交中的快照与我们当前的或 HEAD 提交进行比较,以查看 我们 更改了什么;和
  • 将相同的合并基础快照与 他们的 提交进行比较,看看 他们 发生了什么变化。

由于这三个提交中的每一个在其快照中都有一组文件,因此这些文件就是要比较的文件。

在您的特定情况下,合并库有一个名为 src/a.js.1 的文件,您自己的提交也有一个 src/a.js 文件,并且您有更改了该文件,使其与 src/a.js:

的合并基础副本不匹配

modified in HEAD

他们他们的提交中所做的是遗漏src/a.js完全。因此,当将合并基础 src/a.js 与他们的提交进行比较时,他们 删除了文件:

... deleted in aa1e4d and ...

git merge 的工作是将他们的更改与您的更改结合起来。如果您更改了文件的第 42 行,而他们没有,Git 会将您的更改带到第 42 行。如果他们在文件的前面添加了三行,Git 将接受他们的更改更改以保留额外的三行(这样您的更改现在就在第 45 行)。但他们不只是 修改 文件。他们完全删除了文件

Git 不是 确定 如何正确组合这些,所以它会选择一些事情去做,然后确保停下来并从你那里得到帮助.它告诉你它选择做什么:

Version HEAD of src/a.js left in tree.

left in tree 部分在这里很重要。


1请注意,文件名中有一个嵌入的斜杠。这就是 Git 命名文件的方式:这些不是 folders-and-files,它们只是带有斜线的文件名。 Git 中正在进行一些工作以使其更好地识别重命名,并且 Git 现在确实以有限的方式理解这些 代表 folders-and-files 并且 folder-rename 在进行合并时会导致很多 file-renames。但它仍然有点乱 ad-hoc.


当事情出错时,git merge 留下很多零件

同样,合并有 三个输入。这三个输入是提交,所以每个都有很多文件。 Git 管理这些文件的方式是将 每个文件的所有三个副本 放入 Git 调用的 index 或者暂存区.

索引,或暂存区——有时有第三个名字,缓存,尽管最近这个第三个名字几乎消失了:你现在大多把它看作是一个标志,如在 git rm --cached 中一样——是 Git 如何了解文件 。我之前提到过,每个提交都有 all Git 当时知道的文件保存为永久快照。 Git 知道他们是因为他们被列在它的索引中。

索引的内容随着您从提交移动到提交而改变。检查一些提交的行为填充了 Git 的索引,来自您 checked-out 的提交。进行 new 提交的行为会根据 Git 索引中的任何内容进行新提交。所以在这两个步骤之间,您的工作是更新 Git 的索引。

这是很多混乱的根源,在 Git. 索引中包含 Git 实际上是 的文件using.2 您可以在其中查看文件和完成工作的工作树包含这些文件的 。这些副本供您使用。它们不是 Git 的副本!他们是你的。 Git 将要使用的副本在 Git 的索引中。当您 运行 git commit 或以其他方式进行新提交时,index 副本将被 Git 使用。

当您更改某个文件的 working-tree 副本时,您必须 运行 git 添加 <em> 文件</em>一直。原因很简单:git add 意味着 使索引副本匹配我更新的 working-tree 副本 。如果该文件之前在索引中,那么现在它已更新。如果该文件之前根本不在索引中,那么现在是了。所以无论哪种方式,在 git 添加 <em>file</em> 之后,索引副本都会更新。

这一切归结为一个很好的简化:Git 的索引包含您的提议的下一次提交。如果你想 完全删除 一个文件,这样它就不会出现在 下一个 提交中,你只需 运行 gitrm<em>文件</em>。这将从两个地方删除文件:您的工作树,您可以在其中查看和使用该文件作为常规普通文件,以及 Git 的索引,其中保留副本以在 下一步提交。

git merge命令乱七八糟这张漂亮的简单图片。索引不再只包含每个文件的 一个 副本,现在索引包含每个文件的 最多三个 副本。这三个副本来自正在合并的三个提交。


2从技术上讲,索引包含文件 names——用正斜杠完成,例如 src/a.js——以及相应的 blob 哈希 ID。它还包含大量缓存数据,可帮助 Git 运行得更快。内部 Git blob objects 都是 de-duplicated,因此文件在 re-use 相同文件内容的提交中得到 共享 。这意味着索引本身并不真正包含 files。但您可以将其视为 Git 内部格式的文件副本。只有当您开始使用 git update-indexgit ls-files --stage 直接查看索引中的内容时,这种错觉才会被打破。


git merge 如何使用 Git 的索引

简化图——实际更复杂——你可以认为git merge是这样工作的:

  1. 确保索引和工作树是“干净的”,即每个文件的 HEAD 副本是 Git 索引和您的索引中的副本工作树。

  2. 展开索引。通常,每个文件 in 索引都在“槽零”中。每个文件有四个插槽,但通常插槽 1、2 和 3 未使用。 Git 现在将所有文件从插槽 0 移动到插槽 2,Git 有时调用 --ours

  3. 将合并库中的文件复制到插槽 1,并将其他提交中的文件复制到插槽 3。索引现在包含所有三个提交的每个文件的所有三个版本。插槽 1 是合并基础插槽 - 它没有 --base 名称,但也许应该有 - 插槽 3 有时称为 --theirs。 (插槽 2 当然是 --ours,如步骤 2 中所述。)

  4. 当所有三个插槽中的所有文件 匹配 时,此文件的合并为 super-trivial。只需将(单个)文件放回零槽,擦除剩余的槽:反正这三个槽都是一样的。

  5. 我们没有三个 个文件都匹配,但如果其中两个匹配怎么办?这里分三种情况:

    • 我们的和他们的匹配(但不同于 slot-1 副本):我们都对文件进行了 相同的更改,所以使用哪个这些都很方便。将那一个放到零槽并擦除其他槽,我们就完成了。

    • 他们的和 merge-base 副本匹配:他们没有更改文件,而我们做了。使用我们的文件版本:将其从插槽 2 拖放到插槽 0。擦除其他插槽,我们就完成了。

    • 我们的和 merge-base 的副本匹配:我们没有更改文件,而他们更改了。使用他们的文件版本:将其从插槽 3 拖放到插槽 0。也将其复制到工作树中,以便我们可以看到新文件。擦除其他插槽,我们就完成了。

  6. 剩下的案子比较难

所有剩余的案例都需要一些实际工作,当他们罢工时,必须做更多的工作。 (上述情况由 Git 中的特殊 index-only 代码处理。这实际上是一个特别烦恼的来源,与低级合并驱动程序有关:他们没有 运行在任何简单的情况下都可以。)

最常见的情况是,对于案例 6,我们和他们都对一个特定文件进行了一些更改。 Git 将尝试 合并 这两组更改,并将合并的更改应用于合并基础副本(在插槽 1 中)。如果 Git 能够自己进行合并,Git 会将合并后的文件写入我们的工作树,将合并后的文件移至槽零,并擦除三个 higher-numbered 槽。此合并冲突现已解决:Git 自行合并文件。3

但是,在某些情况下,Git不能不会自行解决冲突。这包括我们和他们都更改了相同行但进行了不同更改的情况。在这些情况下,Git 将向文件的工作树副本写入 marked-up diff,其中 <<<<<<< HEAD>>>>>>> theirs 行添加到 Git 的位置无法自行解决。但它也包括我喜欢称之为 high-level 冲突的东西:low-level 冲突的对立面。其他人称这些 树冲突 。这些 high-level 冲突包括像您这样的情况,您更改了文件,但他们 删除了 文件。

对于这些情况,Git 在 Git 的索引中保留尽可能多的副本。在这种情况下,它将在 Git 的索引中保留 src/a.js 的合并基础副本,并在 Git 的索引中保留 src/a.jsHEAD 副本.是我即将 阅读 Git 打印的消息 :

CONFLICT (modify/delete): src/a.js deleted in aa1e4d and modified
in HEAD. Version HEAD of src/a.js left in tree.

这会告诉您 Git 留在您的工作树中的内容:来自 HEAD 提交的副本,即来自您当前分支的副本。


3当 Git 解析了这样的文件时,它是在 line-by-line 的基础上使用简单的文本规则完成的。 Git 不了解文件的内容。这意味着即使 Git 自行解决了冲突,结果 也可能 完全是无稽之谈。实际上,这实际上适用于数量惊人的大量情况。如果没有,您有时可以通过编写自己的 low-level 合并驱动程序来帮助 Git,尽管这很重要。

事实上 Git 在没有实际 理解 文件的情况下执行此操作,这就是为什么 测试合并结果很重要。即使 Git 认为一切顺利,也可能并非如此。


你的工作是修复Git的索引

无论发生什么冲突以及 Git 在您的工作树和 Git 的索引中留下什么,您现在的工作是填写 Git的索引与正确的最终合并结果。 当然,在这种情况下,Git 的索引已经部分或什至大部分充满了正确的东西。这里,Git 的索引在两个槽中有一个 src/a.js 的副本:槽 1(合并基础)和槽 2(--ours 槽)。如果正确的结果是从新提交中省略该文件。

Git的索引rest大概已经全部正确了。如果是这样,您无需对任何这些条目执行任何操作。它只是 src/a.js 的条目 messed-up 并且存在冲突。它有一个插槽 1 条目和一个插槽 2 条目。 Git 本身并不关心您如何执行此操作,但您必须擦除编号较高的插槽并放入 slot-zero 条目,或者擦除所有插槽以使文件不存在。 Git 为此提供的两个主要工具是 git addgit rm

记住,git rm 意味着 从索引中删除并删除 working-tree 版本 。因此,如果正确答案是“完全删除 src/a.js”,您只需 运行 git rm src/a.js。不过,在您的情况下,这不是正确的答案:

I want to keep what is there

这意味着您希望 src/a.js 的某些版本位于槽零中。如果您的工作树中有 正确的 版本,请记住 git add 意味着 使索引副本与工作树副本匹配 。所以:

I want to keep what is there in the HEAD

这就是 Git 留在工作树中的内容。那么,您所要做的就是 运行 git add src/a.js,获取工作树副本(现在也在插槽 2 中)并让 Git 将其写入插槽 0。填充或擦除槽零 会擦除所有编号较高的槽。

所以:

git add src/a.js

src/a.js 的 working-tree 副本复制到插槽 0 的索引,并删除插槽 1 和 2 中的条目。插槽 3 中没有条目,因此现在已解析此文件。

如果您有 more-standard 冲突,Git 会在所有三个位置中保留所有三个副本,加上它自己的合并尝试,并在您的工作树中完成冲突标记.在这种情况下,您可以编辑工作树副本以产生正确的结果,并使用 git add 擦除所有三个非零槽并在槽零中提供正确的文件。但是你不需要做那么多工作,因为工作树副本已经正确了。只是放错了位置!