在 git 与 meld 合并期间,如果只保存 MERGED,为什么我可以修改 LOCAL 和 REMOTE?

during a git merge with meld, why can I modify LOCAL and REMOTE if only MERGED will be saved?

根据这个问题的答案之一 ,LOCAL、BASE 和 REMOTE 文件在合并过程中不会被更改,而只会更改生成的 MERGED 文件。

在 meld 合并期间,我会通过从左侧 (LOCAL) 和右侧 (REMOTE) 移动代码来修改中间面板 (BASE)。我知道 BASE 将是最终合并文件的“预览”,但不会直接保存,这似乎是合乎逻辑的安全步骤。

但是,我也可以将代码从 BASE 移动到 LOCAL 或 REMOTE,并且当我关闭 meld 时,系统会要求我保存对所有三个文件的更改。如果只有 BASE(即 MERGED)与合并过程相关,为什么我可以这样做? LOCAL 和 REMOTE 中的修改会发生什么情况?

TL;DR 的 TL;DR

Git 不使用你的工作树文件,除非你(或其他东西)运行(s) git add。请注意,git mergetool 运行s git add 只有 一个 meld 使用的文件。所以你可以写任意多的额外文件。 Git 不在乎。当 meld 完成时,它只关心 一个特定文件

TL;DR

大概您是通过 git mergetool 运行 使用此合并工具 meldgit mergetool 的工作方式非常简单,一旦您理解了 merge 本身的工作方式,这就是您可以修改所有这些文件的原因:因为它们 只是文件 .

要使所有这些都有意义,您需要了解 git merge 的工作原理。这让我们了解了以下区别:

  • 提交,这是 Git 实际存储内容的方式;
  • Git的index,有三个名字;它参与提交,并在合并过程中发挥更大的作用;和
  • 您的工作树work-tree(两个名称指的是同一事物),其中包含您的文件, meldvim 之类的程序实际上可以查看和编辑。

其中的第三个——您的work-tree——是唯一存放您可以看到的文件的地方。但是——这非常重要——你的 work-tree 根本不在 Git 中 。它只是 Git 将文件粘贴到其中的地方,以便您可以看到它们并处理/使用它们。稍后,git add 会将这些文件中的 一个 复制回 Git 的索引中。如果您使用 git mergetool 到 运行 合并工具,git mergetool 代码 运行s git add 适合您。

运行s git addmerged 文件(按名称)上的合并工具脚本 in 该文件就是 git added 的内容。就 Git 而言,任何剩余的文件都只是垃圾:它们只是未跟踪的文件。我相信 mergetool 应该清理垃圾文件(但 should 并不意味着 always will 并且在 should 部分也是;这里有一个“保留备份”选项,我从未使用过)。

您可以跳过下面的某些部分,具体取决于您对 Git 的熟悉程度。我会尽量缩短它们(通过省略很多内容),但它们仍然会很长。

关于提交的更多背景

每个 Git 提交都有一个唯一的编号。这些数字不是简单的计数——我们没有提交 #1 之后是 #2,然后是 #3,依此类推。相反,这些数字是 random-looking,又大又丑 哈希 ID,由加密哈希函数计算得出。这些数字在 所有 Git 存储库中都是唯一的 (这就是 Git 管理提交的分布式性质的方式),但我们在这里需要知道的是提交已编号。

每个提交包含两件事。提交的所有部分都是 read-only,所以这些东西是不可更改的,并且永远有效——或者至少只要提交本身继续存在:

  • 每个提交都有每个文件的完整快照,以只有 Git 可以读取的特殊存档格式存储。 (这种格式是压缩的,通常是高度压缩的,并且 de-duplicates 文件内容。它可以存储您的 OS 可能无法有效使用的文件,甚至在某些情况下检查出来;在那些情况下,合并将很难或不可能。)提交中的文件由 Git 的索引中的内容决定,如下一节所述,当时某人 运行s git commit.

  • 每个提交也有一些元数据,或者关于提交本身的信息。这包括作者的姓名和电子邮件地址,以及提交者的姓名和电子邮件地址。每个都有一个 date-and-time-stamp。日志消息有 space,由进行提交的人编写,以描述 为什么 他们进行此提交。并且,为了 Git 可以将提交向后串在一起,每个提交记录其父提交的哈希 ID。

合并提交只是其中至少有两个父哈希 ID 的提交。 git merge 命令通常在末尾进行这样的提交:第一个父项与任何普通 non-merge 提交的父项相同,第二个父项是您刚刚合并的提交的哈希 ID (例如,您通过 branch-name 合并的 b运行ch 的提示提交)。合并的 snapshot 部分与任何提交相同:它只是合并完成时记录在 Git 索引中的每个文件的完整副本。

Git 的索引,以及它在合并期间如何扩展

Git 的 index 有三个名称:Git 称它为索引(正如我在这里所做的那样),暂存区(至少对于正常提交),并且——罕见y 这些天,主要是像 --cached 这样的标志—— 缓存 。对于正常的 non-merge 提交,我喜欢将索引描述为持有您的 提议的下一次提交 .

索引中的内容通常是一个元组列表:名称、模式和哈希 ID:

  • 名称是一个文件名,以正斜杠结尾,如 top/sub/file.ext。在这个级别,Git 不会“考虑”保存文件的目录:它只有包含斜线的长名称文件。即使在 Windows 上,这些斜杠也会向前,即使 Git 必须将这样的文件放入名为 file.ext 的文件中,该文件位于名为 top 的文件夹中,其中包含一个子文件夹 [=38] =],Windows 更愿意表达为 top\sub\file.ext。该索引在内部坚持使用正斜杠。 (这通常不会向用户显示,这只是理解 Git 阻止它存储空文件夹的问题的一种方式。这样的事情在 Git 中根本不存在索引:索引仅包含 个文件。)

  • 模式,对于一个普通的文件,真的只是记住它是+x还是-x:可执行文件,还是non-executable文件。对于 hysterical reasons,这分别存储为 100755100644

  • 散列 ID 与 Git 如何在内部存储文件内容有关,作为 blob 对象。这些东西是压缩过的read-only,如果对象存储为打包对象,可能是even-more-compressed using delta encoding.

同样,这是正常的 non-merge 情况。这些条目的阶段编号(因为索引是“临时区域”)始终为零。这就是他们正常的原因。

git merge 启动时,它 扩展索引 。它替换了所有 stage-zero 条目,这些条目代表 当前提交 ——索引需要在合并操作开始时匹配当前提交——与 stage 2 个条目。这也会为 stage 1stage 3 条目打开 spaces。我们将在下面回到这一点。

你的工作树

提交的文件(通过 blob 哈希 ID 存储)和索引(字面上存储这些相同类型的 blob 哈希 ID)存储 内部格式 版本的 Git 文件,其中的内容被压缩 de-duplicated,甚至 delta-encoded。这种格式适合存档(因为它是压缩的并且 de-duplicated)但不适合完成任何实际工作。因此 Git 必须 提取 这样的文件,从提交或 Git 的索引中扩展任何压缩。

提取存档 blob 对象的结果进入普通文件。这些文件需要存在于某个地方,而那个地方就是您的工作树。因此 git checkoutgit switch 通过将文件从提交复制到 Git 的索引中来工作——这部分快速且便宜,因为索引以与提交相同的格式保存文件——然后到你的工作树。

复制到你的工作树是缓慢的,但 Git 开始作弊。因为索引 跟踪 工作树中的内容,所以 Git 通常可以很快判断工作树文件是否未被 last结帐。它还可以通过检查哈希 ID 来判断您现在检出的新提交中的文件是否与您之前检出的旧提交中的文件相同。如果一切顺利——通常是这样——Git 可以不理会文件,所以它确实如此。

那么,原则上,不同提交的 git checkout 必须删除所有旧文件(来自 Git 的索引和您的工作树),然后从新提交。 Git 只是跳过了很多这项工作,这意味着 multi-megabyte 或千兆字节校验可能需要很少的时间(有时只需几毫秒,但这在很大程度上取决于 OS、缓存和其他详细信息,以及从提交 X 到提交 Y 的切换不需要更改大量工作树文件)。

除此之外,您的工作树只是一组常规的旧文件和目录/文件夹(您喜欢哪个术语)。在您的计算机上运行的一切,都在这里运行。除了在你告诉它时将 写入 之外——例如,使用 git checkout——Git 只是让你尽情地玩它。然后你可以运行git status,它只看着它,或者git add,它从它复制 进入 Git 的索引。但是,在您执行其中任一操作之前,Git 完全是 hands-off.

简而言之,您的工作树是您的,随心所欲。您可以在此处创建 Git 永远不需要知道的文件。只要 (a) 你不 git add 他们并且 (b) 他们永远不会从一些现有的提交中出来,他们永远不会 into Git's指数,和Git 从来不知道他们。 git status 命令会 抱怨 它们,你需要在 .gitignore 中列出这些文件才能使 Git 关闭哔哔,但除此之外,它们完全无关紧要。

three-way 合并的内部结构

当我们 运行 git merge 时,我们通常会进行 three-way 合并,这可能会产生冲突。要了解发生了什么,让我们看一个示例 提交图 ,即在某些 Git 存储库中找到的一组提交。因为真实提交的哈希ID是不可理解的,所以我们将使用单个大写字母来代表它们,像这样:

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

我添加了两个 b运行ch 名称,branch1——我们目前已经签出,即我们正在使用提交 J 来填充 Git 的索引和我们的工作树——以及 branch2,它选择提交 L(HEAD) 表示法表明我们已经 branch1 签出。所有六个列出的提交都是普通的 single-parent 提交,因此从提交 J 来看——即 git log 如果我们现在 运行 它——我们看到,作为历史,先提交 J,然后提交 I,然后提交 H,然后提交 G,依此类推。从提交 L 来看——如果我们 运行 git log branch2——我们首先看到提交 L,然后是 K,然后是 H,然后是 G,以此类推。

这两个提交历史 相遇 ,当我们像这样倒退时,在提交 H。因此,提交 H 是此 three-way 合并中的 合并基础

合并的目标 是合并工作。我们希望 Git 自己弄清楚 自提交 H 以来我们改变了什么。这些是“我们的改变”。我们希望 Git 弄清楚自提交 H 以来 他们 发生了什么变化。这些是“他们的变化”。 Git 实际上可以做到这一点,使用 git diff:

git diff --find-renames <hash-of-H> <hash-of-J>

这将生成我们更改的每个文件的列表,以及需要删除哪些行并将其添加到每个文件以将提交 H 中存在的那些文件的副本转换为J.

中存在的相同文件

同理:

git diff --find-renames <hash-of-H> <hash-of-L>

将生成一个文件列表它们已更改,以及这些文件中需要修改的行。

如果 Git 简单地(简单地?)组合这两个列表并对从提交 H、Git 将到达一组文件,这些文件保存我们的更改 (H-to-J) 并添加他们的更改 (H-to-L)。在许多情况下,我们更改的某些文件在他们这边将有 no 更改,反之亦然。这些对于 Git 来说很容易。在某些情况下,一些文件会在两面都有变化。如果这些更改涉及 不同的 行,Git 可能能够自行组合这些更改。

无论如何,这些都是Git使用的规则。它只是:

  • 提取(进入 Git 的索引)H 中的每个文件:这些进入插槽 1 条目。
  • 提取(到Git的索引中)J中的每个文件:这些进入slot-2条目。当然它们已经在 slot 0 中了,所以不需要提取; Git 可以将插槽 0 条目移动到插槽 2。 (当使用 git cherry-pick -n 或类似的东西时,Git 确实只需要移动插槽条目,因为这些情况不需要索引匹配任何东西。但这是 git merge 确实需要的特殊情况通常不允许。)
  • 提取(到 Git 的索引中)L 中的每个文件:这些文件进入插槽 3 条目。

索引现在有每个文件的 三个 个副本,来自合并基础提交(BASE),--ours 提交(LOCAL) ,以及他们的 (REMOTE)。对于内部 Git blob 对象,其中的每一个实际上只是一个哈希 ID(好吧,加上名称和模式,以及代表插槽的暂存号)。1

由于 de-duplication 技巧,如果 没有人对文件进行任何更改 ,所有三个暂存槽将拥有相同的哈希 ID(和模式)和 Git 可以将所有三个索引条目折叠回单个 slot-zero 条目。如果我们更改了文件,但他们没有,则基础和他们的插槽将具有相同的哈希 ID(和模式),而我们的将不同,Git 将只获取文件的 our 版本,将插槽 2 移动到插槽 0 并擦除插槽 1 和 3。如果 they 更改了文件,但我们没有,基础和我们的插槽将具有相同的哈希 ID,它们的哈希 ID 将不同,Git 将只采用文件的 他们的 版本,将插槽 3 移动到插槽 0,等等

这意味着我们只需要为双方都进行更改的文件努力工作(好吧, high-level / 树冲突,我将在此处跳过)。在这种情况下,Git 今天的各种合并策略通过:

  • 调用合并驱动程序,如果有的话:这个程序必须完成这项工作;或
  • 调用 built-in low-level 合并驱动程序,否则。

built-in low-level 合并驱动程序在 line-by-line 基础上工作,对单个文件使用 git diff2对于您在 git diff 输出中看到的每个 diff-hunk,它会查看另一侧是否触及 相同的线 ,或“触及”另一侧的线更改(例如,如果“我们的”diff 在末尾添加一行,而“他们的”diff 也在末尾添加一行,Git 不知道要使用哪个 order当添加两组线时)。3 它写入,到我们的工作树副本相关文件,Git最好猜测正确的合并。如果这一切顺利——如果 Git 能够组合两组更改而不会发生冲突——Git 然后对文件执行内部 git add。如果不是,Git 将冲突留在文件的工作树副本中 ,完成冲突标记,并且 不会 文件上的内部 git add

当低级驱动遇到被认为是冲突的东西时,如果有一个extended-argument -X ours-X theirs 生效,它只会接受我们的改变(从 1 -vs-2) 或它们的变化 (1-vs-3) 根据 -X 值,并且不放入任何冲突标记。所以 low-level 冲突 可以 在使用这些标志的软件中自动解决。但是请注意,Git 在这里没有做任何事情 smart。它只是根据 line-by-line diff hunk 选择 1-vs-2 文件差异或 1-vs-3 文件差异。但这确实让 Git 运行 内部 git add 独立存在。

当Git运行内部git add时,这只是获取文件的工作树副本并将其复制到插槽中零,擦除该文件的插槽 1 到 3。这将文件标记为已解决。对于那一组文件条目,索引收缩回正常。处理完所有文件后,Git 的索引中仍然显示一些冲突(因为某些文件没有得到 pre-collapsed 并且没有得到 git add-ed),或者没有(所有文件都有一个简单的索引崩溃,或者在低级驱动程序完成它的事情后得到 git add-ed)。


1这里的设计本应在进行递归合并时允许多个 slot-1 条目,但那从来没有发生过。目前尚不清楚它是否 可以 去任何地方,因为有一些非常棘手的极端情况,其中三个提交中的一个或两个不存在文件,如果你允许它们会变得更棘手诸如此类。

2在现有的merge-recursive算法中,高层代码和底层代码都存在大量冗余工作。正在进行的新改进合并工作正在消除很多这种情况,并将加速许多更困难的合并。这不会改变合并代码的目标,也不会改变我在这里给出的高级描述,但会改变一些工作完成和结果保存或不保存的点,以便它们可以完成一次而不是重复。

3低级别 union merge,Git 不直接支持,但您可以使用git merge-file,用作您编写的 low-level 合并驱动程序——假设行顺序无关紧要,并且可以处理它而不将其称为冲突。


这一切的结果

合并对 Git 的索引的描述很长,但如果您一直遵循逻辑,您会看到:

  • 任何不能发生冲突的文件现在都处于零阶段。
  • 任何 可能 有冲突的文件,但驱动程序(来自 .gitattributes)或默认 built-in low-level 文件合并是能够自行解决——可能使用 -X ours-X theirs——也处于零阶段。
  • 因此,只有具有无法解决的 low-level 冲突或具有某些 high-level / tree-level 冲突的文件(由于 space 原因,我在此省略),具有非零索引阶段条目。

因此当且仅当 Git 的索引中有任何非零阶段编号时,合并冲突仍然存在。在这种情况下,git merge 停止,留下一堆内部文件——例如 .git/MERGE_HEAD.git/MERGE_MSG——以记录正在进行的合并。同时索引本身有一些非零槽号,记录有冲突。

如果冲突是 low-level 冲突, 我们在某些文件上使用 Git 内置的 low-level 合并驱动程序,该文件的 工作树 副本中有冲突标记。这些标记是通过 git merge-file 可用的相同代码从 运行 三个原始输入文件派生出来的(所以你可以用这种方式重建合并冲突,但有一个更简单的方法此时使用 git checkout -mgit restore -m)。无论文件的工作树副本中有什么,索引中都存在三个输入文件。

如果我们现在 运行 git mergetool,此代码会遍历索引(使用 git ls-files --stage 或等效项)以查找冲突的文件。然后它使用 git checkout-index 提取作为 low-level 合并驱动程序输入的三个文件。这些得到时髦的 .gittemporary 样式名称,git mergetool 重命名为 <em>file</em>_BASE, <em>file</em>_LOCAL,和 <em>file</em>_REMOTE 分别(好吧,确切的命名模式很棘手,这只是一个近似值)。出于内部目的,它将 file 复制到 <em>file</em>_BACKUP。然后它 运行 是您在这些文件上选择的合并工具(不包括备份文件)。

您的合并工具现在可以处理 工作树 文件。 None 这些文件在 Git 中。您可以使用合并工具对它们做任何您想做的事。 file 中的任何内容,git mergetool 均假定这是您使用合并工具生成的结果。

这里,还有一个绝招:

  • 一些合并工具具有“受信任”的退出代码,而另一些则没有。

  • 如果您的合并工具被标记为“受信任”并且退出状态为 合并已完成,请使用结果、Git会 git add 那。这会擦除三个插槽并标记文件已解决。

  • 如果您的合并可信,Git将比较 _BACKUP 文件与工具的输出。如果文件未更改,git mergetool 会询问您是否认为合并有效。只有当你说是时,它才会 git add 结果。

git merge在中间停止时,你的工作是清理混乱,通过写入Git的索引,在槽零,正确的合并结果。您可以按照自己喜欢的方式执行此操作。我的首选方法一般只是在vim中打开file,在Git写入后merge.conflictStyle设置为diff3.我发现大多数冲突很容易通过这种方式解决。在少数情况下,我真的很想获得这三个版本,对于这些情况,git mergetool 一种实现它的方法——但是玩过 git mergetool, 我还没有找到特别 好的 方法。不过,这是 user-preference 交易之一。

无论如何,一旦你解决了所有的冲突,并让运行git add更新Git的索引,你应该运行:

git merge --continue

告诉Git完成合并。 Git 不关心 你如何 解决冲突。 Git 只关心您 将正确的文件放入索引 的暂存槽 0,清除其他三个暂存槽。

在糟糕的过去,你不得不 运行:

git commit

完成合并,如果你感到困惑(例如,被打扰,cd' 到其他存储库,然后开会或其他什么,现在不在什么地方当你 运行 git commit) 你在想你可以做一个普通的提交而不是完成你的合并。 --continue 检查实际上有一个合并要完成,然后 运行s git commit 完成它。