合并 git 中 2 个不同分支中的 2 个同名文件

Merging 2 files with the same name in 2 different branches in git

我目前在名为 branch1 的分支中有一个名为 test1 的文件,该分支是从 master 创建的,另一个名为 test1 的文件位于名为 [=15= 的分支中] 也是从 master 创建的。 如果我合并 master 中的 2 个分支,写在两个文件中的代码会发生什么?

如果您在同一个地方对两个文件进行了更改,将会发生合并冲突。

如果您没有在同一个地方对两个文件进行更改,它将毫无问题地合并(可能在另一个位置发生冲突)。

您可以通过编辑冲突文件、删除提交标记、添加文件并提交来解决冲突。

作为 amer answered(正确),您将在两个合并的 一个 中遇到合并冲突。您将不得不做一些事情来处理合并冲突。做什么取决于你。不过,值得指出的是,为什么 你会得到 一个 合并冲突,而不是两个,以及为什么会发生合并冲突。

Git的合并并不是真正关于分支。这是关于 提交 。大多数 Git 都是关于提交的,git merge 在这里也不例外。

让我们在这里注意提交是什么以及做什么。每个提交都有两部分:它的数据——所有文件的保存快照——和它的元数据,或者关于提交的信息。

  • 保存的快照非常简单:如果您克隆存储库并使用 --no-checkout,您将得到一个空的 work-tree([=274= 没有可编辑或可用的副本]任何 文件)。然后你选择一些提交——任何地方的任何提交——并告诉 Git 检查那个特定的提交,可能是通过它的原始哈希 ID。现在,您拥有所有文件的副本,就像他们在提交时的样子一样。

    一般来说,这就是 git checkout 或 new-in-Git-2.23 git switch 命令的用途:你选择一些提交并说 给我所有文件来自该提交。它们会进入您的 工作树work-tree,您可以在其中查看它们并对其进行处理。您还可以将其他 un-Git-ified 文件放入您的 work-tree,例如编译文件或输出或其他任何内容。这些文件保持 未跟踪(我不会在这里详细介绍,但它们不在 Git 中,它们只是位于您的 work-tree unless/until 删除它们)。

  • 提交中的 元数据 记录了诸如谁提交的信息——他们的姓名和电子邮件地址——以及他们提交的时间。这是您在 git log 输出中看到的内容。

    不过,有一部分元数据专门针对 Git 本身。每个提交都有一个唯一的哈希 ID,每个 Git 都一致同意:一个哈希 ID 用于 that 提交,而不用于任何其他提交。所以很容易判断你是否有一些提交:你只需将它的哈希 ID 给 Git,你的 Git 要么有它,在这种情况下,它有 that 提交,或者你的 Git 没有它,在这种情况下你必须找到其他一些 Git 帽子有它。

    无论如何,每个提交都将其先前或 parent 提交的哈希 ID 作为其元数据的一部分进行存储。大多数提交只有一个。合并提交有点特殊,因为它们有 两个 parent(或更多,但大多数只有两个)。

    这些 parent 提交——或 parents,用于合并——是 Git 存储历史的方式。每个快照只是一个快照,但每个快照还说:而我之前的快照是______(用哈希ID填空)。对于合并,这是 firstparent。对于常规提交,它是唯一的 parent(因此也是第一个 parent)。因此,通过回到每个提交的第一个 parent,Git 可以追溯随时间发生的事情。放两张快照:左边是旧快照,右边是新快照,然后比较它们。 What's different? 这种差异告诉您发生了什么:旧版本和新版本之间发生了什么变化。

一旦您了解了有关提交的内容,我们只需要再添加一件事即可使分支正常工作。 在Git中,一个分支名称记录了最新提交的哈希ID,我们希望调用“部分分支的名称”。 主要是这样——这就是分支名称为我们和 Git 所做的事情。它记录了 last 提交。提交本身记录了历史。

因此,给定一系列提交,在一个只有三个提交和一个分支名称的非常​​小的存储库中,我们有,例如:

A <-B <-C   <--master

最后 次提交是 C。我们使用名称 master 来存储它的实际哈希 ID——这实际上是一些丑陋的 random-looking 字母和数字串,我们永远猜不到。 CommitC本身存储了较早commitB的哈希ID,因此C指向B;提交 B 存储早期提交 A.

的哈希 ID

提交 A 是特殊的:它根本不指向回,因为它是第一次提交,不能指向更早的提交。这就是 Git 知道 停止 返回的方式:当它不能返回时。

我们可以,给定一个 Git 存储库,进入并查找所有提交,并查看哪些是最后提交的,但是拥有一个可以快速找到它们的名称会更快。当我们开始拥有多个分支时,这一点也变得很重要。让我们从一个大约有八次提交的小型存储库开始:

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

现在让我们添加一个新的分支。我们将从让新的 name also select commit H 开始。我们需要一种方法来知道我们正在使用哪个分支,因此我们将特殊名称HEAD附加到其中一个分支名称:

...--G--H   <-- master, feature1 (HEAD)

现在我们将添加一个新的提交,它会获得一些新的 random-looking 哈希 ID,我们将其称为 I:

          I   <-- feature1 (HEAD)
         /
...--G--H   <-- master

当我们添加 new 提交时,Git 自动更新分支名称 以指向新提交。更新哪个分支名称?一个 HEAD 是 attached-to。其他人都留在原地。

现在通过 H 的所有提交都在 两个 分支上,而提交 I *仅在 feature1 上。让我们再做一次提交,然后创建一个新分支 feature2 select 提交 H,并开始使用那个:

          I--J   <-- feature1
         /
...--G--H   <-- master, feature2 (HEAD)

现在让我们向 feature2 添加两个提交:

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

现在,假设在提交 IJ 中,我们创建了一个 new 文件 test1,但尚未提交 H。假设在提交 KL 中,我们 创建了一个名为 test1.

的新文件

正在合并

我们现在要将这两个功能合并到 master 中,一次合并一个。没有明显的原因,1 我们将使用 --no-ff 选项:

git checkout master
git merge --no-ff feature1

实现这个。

当我们git checkout master时,我们将Git指向:

  1. 提取由名称 master 标识的提交——提交 H——到我们的 work-tree(以及 Git 的 index,我们不会在这里讨论);和
  2. 设置我们的 work-tree 匹配,这意味着 删除 文件 test1,它在提交 L 中——有一个使用该名称保存快照文件——但不在提交中 H.

所以,现在我们有:

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

我们准备好 运行 git merge --no-ff feature1

Git 现在发现 三个提交 ,而不仅仅是两个。感兴趣的三个提交是:

  • 我们当前的提交,HEAD。这真的很容易找到,因为 HEAD 附加到分支名称并且分支名称指向提交,所以 Git 找到提交 H.

  • 我们命名的另一个提交。这也很简单:我们说过要合并 feature1。名称 feature1 标识提交 J。 (看看图就知道了!)

  • 合并基地。合并基础由 提交图 定义,由一个提交到另一个提交的 inter-connections 形成。虽然我们不会详细介绍所有细节,但您可以将其视为 最好的 共享 提交,即 上的最佳提交=274=]两个分支。从 J 开始——根据名称 feature1 找到——我们向后工作;从 H 开始,正如 master 所发现的那样,我们也向后工作。当某些提交在 both 分支上时,这是一个 shared 提交。最新的此类提交——这里没有正确定义 newest,但在大多数情况下很明显——通常是 best 提交。2

在这种情况下,合并基础显然是提交 H 本身。


1我将在此处进行的合并是您在 GitHub 上使用其“合并请求请求”按钮获得的那种。从 Git 命令行,您可以获得更多选项。 --no-ff 选项强制 command-line Git 进行真正的合并,而不是使用它的 short-cut “快进 not-really-a-merge” 选项。

2从技术上讲,Git 所做的是在有向图中找到最低公共祖先 (LCA)。在一棵中,总有一个well-defined LCA,但是Git的提交图不一定是一棵树:它只是一个有向无环图或DAG .两次提交可能没有 LCA,或者可能有不止一个 LCA,合并对这些情况做不同的事情。


合并,第 2 部分

找到合并基础后,Git现在 运行s 两个 比较两个提交,看看有什么变化 操作。比较 #1 将合并基础与 --ours 提交进行比较,即与 HEAD 进行比较。所以 Git 会做:

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

显然,提交 H 与提交 H 相同。什么都没有改变!

然后,Git 做第二次 diff,看看“他们”(我们)在另一边改变了什么:

git diff --find-renames <hash-of-H> <hash-of-J>   # what they changed on feature1

那么merge的作用就是合并这两组变化。在我们更改了一些文件而他们没有更改的地方,Git 接受了我们的更改。他们更改了一些文件,而我们没有,Git 接受了他们的更改。 这些组合更改会应用到 merge-base 快照。 这样,我们保留所有工作并添加他们的工作——但无论我们和他们不同 对某些文件或多个文件进行更改,Git 将显示 合并冲突

在这种情况下,--ours diff 是完整的tely empty:我们没有改变任何东西。因此,无论“他们”——实际上,我们 feature1——做了什么,Git 都会进行这些更改。这包括添加一个新文件 test1。这种合并进行得很顺利,所以 Git 自己进行新的合并提交。

新合并提交的 第一个 parent 是我们在 master 上的当前提交 H。新合并提交的第二个 parent 是他们在 feature1 上的提交 J。我们可以画出来——这里的图没有正确显示第一次提交和第二次提交,但是如果需要的话我们可以记住它,或者询问 Git 关于两个 parent 的问题,看看哪个是第一个, 或者其他什么。

结果如下所示:

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

请注意 other 分支名称是如何移动的:我们仍在 master,它已移动到指向 M,并且 feature1 仍然命名为提交 Jfeature2 仍然命名为提交 L.

合并冲突

如果我们现在 运行 另一个 git merge——这次是 feature2——Git 将再次找到三个提交:

  • 当然,我们和他们的提交是提交 ML
  • 合并基础是最好的共享提交。

看图。 master feature2 都有哪些提交?提交 G-H-I-J-M 全部在 masterH 两种 方式,直接来自 M 的第一个 parent,间接地从 JI 再到 H 通过 M 的第二个 parent——因此 G 有两种方式,等等,但我们真正关心的是 HG 在那里。

同时,feature2 结束于 L,回到 K,然后回到 H。所以提交 HG 都是共享的。不过,提交 H 最好的 之一。然后,合并基础再次提交 H.

Git 将再次 运行 两个 git diffs,都带有 --find-renames(检查重命名的文件)并且都从 H 到两个分支提示。所以 Git 将把 H 中的快照与 M 中的快照进行比较,看看我们改变了什么。

我们从 HM 改变了什么?好吧,在 M 中,我们添加了通过比较 HJ 得到的所有更改。因此,我们在 feature1 更改 的任何文件都在 M 中更改。但是我们还在 IJ 中添加了一个新文件 test1,所以这个 change-set 说 add all-new file test1.

当我们比较 HL 时,也表示 添加一个 all-new 文件 test1。所以两个变更集都说要添加一个新文件

Git 将这种冲突称为 add/add 冲突 在 work-tree , Git 只是将两个文件的全部内容作为冲突留给您。您必须以某种方式解决此冲突。你如何去做取决于你。无论您选择将什么放入文件 test1,您现在都可以 运行:

git add test1

和 Git 将假定文件 test1 中的内容是该冲突的 正确 解决方案。

一定要编辑文件!如果你不这样做,它只是其中有冲突标记,Git 认为这是 正确答案! 可能不是。

一旦您解决了所有冲突,并确定合并结果是正确的——例如,您已经完成了您需要做的任何测试——您可以通过 运行ning 安全地完成合并:

git merge --continue

或:

git commit

(git merge --continue 只是确保您仍在完成合并,然后 运行s git commit 为您服务,所以他们最终会做同样的事情——除非您已经即完成或终止合并。)

Git 现在将进行 另一个 合并提交;我们将其称为提交 N,并像这样绘制它:

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

N的第一个parent是MN的第二个parent是L。现在有 三种 种方法可以从 NH,并且图中的 所有 提交都在 master.

现在删除名称 feature1feature2 是安全的,因为 Git 可以通过向后查找这些提交——包括 JL来自提交 N。如果您想保留直接快速地找到提交 JL 的能力,您 没有 删除名称,但它们不是更长必要,就像它们在合并操作之前一样。