解决 Git 中的合并冲突时的新文件

New files while resolving the merge conflict in Git

我是 Git 的新手,如果这听起来像是一个微不足道的问题,我们深表歉意。我昨天在我的特色分支的 Git 中提交了一些代码。今天审核通过的时候,我正在尝试合并到develop和getting merge冲突。

今天我在开发分支,它是最新的并且有一些新的新文件由另一个开发人员提交。当我从 develop 切换到我的功能分支时,我看到 develop 分支中的所有新文件都是我 IDE 中的新文件。所以我应该只提交有冲突的文件,还是在解决冲突后我必须提交所有新文件。

这个问题似乎是关于如何使用你的 IDE 的一半,但请注意 Git 本身将 所有 文件存储在 个快照。这包括合并快照。

因此,如果您在提交之前从字面上从合并中删除文件,合并结果将没有 包含新文件。实际上,您的合并将声称合并这些文件的正确方法是删除它们。因此,它们应该保持 "new files",除非合并它们的正确方法 删除它们。

更多详情

重要的是,在使用 Git 时,同时记住很多事情。 (这是 Git 难以上手的原因之一。)以下是一些需要了解的重要项目的列表:

  • Git 与 文件 无关,甚至与 分支 无关。 Git 就是关于 提交 。这意味着您需要确切地知道提交是什么以及做什么。

  • 每个提交包含两件事:它的主要数据,以所有文件的快照形式;和一些描述此提交的 元数据 或 data-about-the-data。元数据包括谁创建的、何时创建的,以及——对人类很重要,尽管与 Git 本身无关——为什么 你(或任何人)进行了提交。它们还包括一些 提交哈希 ID。这些哈希 ID 是 super-important 到 Git,尽管您自己可能并不关心它们。

  • 每次提交都会获得一个唯一的哈希 ID。可以说,每个提交的 "true name" 就是它的哈希 ID。这些哈希 ID 是 Git 查找提交的方式。如果你想获取 Git 的 out 文件,你将使用哈希 ID,即使它被名称遮盖了:分支名称如 masterdevelop,或类似 v2.1 的标签名称,或其他名称。

    这些哈希 ID 又大又丑,人类无法处理。它们 需要 又大又丑,因为每个提交都有一个唯一的哈希 ID。显而易见的方法,只是按顺序对它们进行编号(提交 #47 将在提交 #46 之后,等等)可能会起作用,除了 Git 是 分布式 的事实。没有中央 Git Commit Number Assigner 每个人都可以去获取下一个号码。

    由于它们又大又丑,我们一般不去看它们。我们使用名称,我们稍后会详细说明。

  • 每个提交——嗯,几乎每个提交——都有一个 parent 提交。 parent 是在此之前的提交。这就是这个额外元数据的意义所在:提交存储其 parent 的哈希 ID。合并提交在一个方面是特殊的:它们存储 多个 parent,即,在此之前有多个提交。

    (某人所做的第一个提交没有 parent,因为没有更早的提交。此提交称为 root 提交。每个 non-empty 存储库至少有一个,并且有多个是不常见的,尽管可以进行新的根提交,或以其他方式获取它们。)

  • A branch name,和master一样,只记得last的raw hash ID在分支中提交。

  • 因此,通过添加新提交,分支增长。添加一个新的提交包括制作一个新的快照——每个文件的新副本——其parent设置为当前提交 . Git 然后让分支名称记住 new 提交的哈希 ID。

因此我们可以将提交画成一条链,最新的提交在右侧(或 git log --graph 的顶部),如下所示:

... <-F <-G <-H   <--master

在这里,每个大写字母代表一些丑陋的提交散列。 最新 提交的散列是HH 包含其parent G 的散列ID。 Git 可以使用 H 中的哈希 ID 来查找 G。提交 G 包含其 parent F 的哈希 ID,因此 Git 可以找到 FF 包含其 parent 的哈希 ID,依此类推。

但是我们如何找到H呢?这就是 分支名称 的来源:分支名称仅包含 last 提交的哈希 ID。

因此,为了向 master 添加提交,Git:

  • 写出提交,包括 parent 哈希 ID H
  • 计算一个新的唯一哈希 ID,我们称之为 I
  • 然后 Git 填充到 分支名称 master,给出

    ... <-F <-G <-H <-I   <--master
    

提交是快照,但您查看更改

当我们有像这样的线性提交链时

...--F--G--H   <-- master

我们要求Git显示我们提交H,我们看到更改,而不是快照。但那是因为那是 有用的,所以 Git 实际上提取 both 提交 GH 到临时区域(真的在记忆中),然后 比较 它们。

两个提交,GH,是两个快照。两者都包含您的所有文件。 GREADME.md 的副本和 H 中的副本可能 不同 ,但是,在这种情况下,向您显示 H、Git 向您显示 G 中的副本与 H 中的副本之间的 差异

当然,您可以在两次提交中使用不同的文件。也许 GH 都有 README.md——也许它们在两个提交中是一样的——但也许 Hfile.py 而不是G 完全没有。在这种情况下,G-vs-H 显示 新文件

请注意,您也可以在 G 中包含不在 H 中的文件;在这种情况下,如 Git 那样进行比较,会告诉您该文件已 已删除 。它仍然作为完整快照存在于提交 G 中。 H.

中没有它

多个分支

当你有多个分支名称时,你所拥有的是你可以这样画的东西:

          I--J   <-- master
         /
...--G--H
         \
          K--L   <-- other

两个名字,masterother,select两个按哈希ID提交。提交 Jmaster 的提示——那里的最后一个——提交 Lother.

的提示

现在我们有两个分支 names,我们需要一种方法来记住我们实际使用的是哪个分支。 Git 为此使用特殊名称 HEAD,将其附加到您存储库中的分支名称之一:

          I--J   <-- master (HEAD)
         /
...--G--H
         \
          K--L   <-- other

请注意,包括 H 在内的提交都在 两个分支 上。 (Git 在这里不常见;大多数版本控制系统不是这样工作的。)提交 IJ 仅在 master 上,并且——至少现在——提交 K-L 仅在 other.

合并就是合并工作

当你在你的分支上,并且你正在合并其他人在其他分支上所做的工作时,你不想只在他们的最新提交中获取他们的文件 as-is,也不想只获取你的files as-is 在你最近的提交中。您想要合并您所做的更改他们所做的更改。

由于 Git 仅存储 快照 ,但是 Git 如何找到更改?我们已经看到 Git 如何将提交与其 parent 进行比较。但假设您有:

          I--J   <-- master (HEAD)
         /
...--G--H
         \
          K--L   <-- other

我们如何比较您在 master 上所做的更改与他们在 other 上所做的更改? Git对此的回答是:找到最好的共享提交,它在两个分支上。 在这里,这显然是提交 H。所以 Git 现在将 H 中的所有文件与 J 中的所有文件进行比较:

git diff --find-renames <hash-of-H> <hash-of-J>   # what we changed

然后,Git 将 H 中的所有文件与 L 中的所有文件进行比较:

git diff --find-renames <hash-of-H> <hash-of-L>   # what they changed

Git 可以合并 变化 。无论我们做了什么,Git 都可以对 H 中的文件做同样的事情,但也可以做 他们H 中的文件做的任何事情.

合并冲突

但有时,在尝试将这些更改结合起来时,Git 运行 会成为一个问题。例如,如果我们更改了 README.md 的第 42 行,他们也更改了 README.md 的第 42 行,但我们做了 不同的更改? 在这种情况下,Git 尽其所能进行合并,然后因 合并冲突 .

而停止

你现在的工作是解决这些冲突。 Git 的 conflict-resolving 能力有限,但它提供了一系列质量各异的工具来提供帮助,并允许您在上面添加自己的工具。很多 IDE 添加了很多工具,质量参差不齐,而且我对其中大部分都无话可说,因为我不使用它们。

尽管如此,您很有可能 运行 git status 在解析过程之后的 and/or 期间。这个 git status 根据您在解析过程中所处的位置说不同的事情。我在这里假设你已经完成了解决——git status所有冲突都已解决 或者没有说任何关于 未合并的文件。 (精确的输出取决于你的 Git 年份;旧的 Gits,从 2.x 系列之前,在这里几乎没有那么好,任何超过 1.8.4 的东西真的不是好。)

当您此时使用 git status 时,Git 正在比较您的 提议的下一个提交 ,这将是一个合并提交——我们还没有尚未绘制或描述 — 当前提交。也就是说,你还处于这种情况:

          I--J   <-- master (HEAD)
         /
...--G--H
         \
          K--L   <-- other

但是,在table,提议 进行新的提交 MM 中的 snapshot 将不同于 Jgit status 中现在存在的 snapshot会告诉你这件事,就像 Git 可能会告诉你 J 和这个提议的 M.

之间的区别一样

现在,假设在 HL 的差异中,Git 发现 他们 添加了 一些新文件。这些文件不在 H 中,而在 L 中。这些文件可能不在 IJ.

因此,Git 从 L 中获取了这些新文件,它们现在在您 提议的下一次提交中 。如果您现在进行合并提交 M,这些文件将存在。比较 J 与此提案,这些文件是 新文件

所以git status会告诉你,他们添加的文件是新的!您 可能想保留它们 。如果您现在 删除 它们,它们将从您提议的新提交中消失。

您仍处于 "resolving" 这一步,无论您是否认为自己已经完成。您已经告诉 Git Git 抱怨的每个冲突文件都已完成。他们已解决的版本已准备好进入新提交,并且 git status 会将 J 中的文件与提议的新提交中的文件进行比较,并说它们不同(如果它们不同)。但是,在这种状态下,您可以继续进行更多更改——不是来自提交的更改 J L.

进行更多更改很少是个好主意。人们将此时所做的更改称为 邪恶的合并 。有关更多信息,请参阅 Evil merges in git?。您 可以 做,如果您觉得有充分的理由去做,也许您毕竟应该做。请记住,当您进行新的合并提交时,您有机会解释您这样做了,以及 为什么 您这样做了。但您可能希望将新文件作为新文件保留在这里。

无论如何,您现在完成合并,使用 command-line Git,使用:

git merge --continue    # or git commit, if your Git does not have --continue

这就是最终的合并提交。正如我们之前提到的,一个 merge commit 有两个 parent(技术上,两个或更多,但你可能不会遇到这些 so-called 章鱼合并你自己)。 merge的firstparent就是通常的parent。第二个是你告诉 Git 合并的提交:

          I--J
         /    \
...--G--H      M   <-- master (HEAD)
         \    /
          K--L   <-- other

新提交 M 现在有两个 parent,以及像任何提交一样的快照,作者和 date-and-time-stamp 以及日志消息等等。第一个 parent 是 J,第二个是 L 因为你说 git merge otherother names commit L.

查看合并提交是不同的

当您稍后查看此提交时,默认情况下,Git 不会 显示更改内容。那是因为 Git 不知道要与哪个 parent 进行比较。 Git 是否应该提取 JM,然后比较这两个?还是应该提取 LM 并进行比较? git log -p 命令是惰性的,只是不执行任何一个。

还有其他 Git 命令和其他查看更改的方法,可让您选择要使用的 parent。最简单的就是在git log -p上加上-m。那就是说:当你点击合并提交时,运行 每个 parent. 一个差异,也就是说,git log 现在将首先比较 J-vs-M,并证明;然后比较 L-vs-M,并显示。但你必须要求这个。

您应该知道 git show 将使用 Git 所谓的 组合差异 显示合并提交。但合并差异故意 省略 很多细节。大多数情况下,他们试图显示发生或可能发生合并冲突的区域。