Git 合并冲突 将远程更改也添加为本地更改

Git Merge Conflicts Adds the remote changes too as Local changes

我有一个奇怪的合并冲突问题,我不确定为什么它只发生在某些存储库上,(可能是由于合并规则上的回购设置)。

通常,如果我有合并冲突,我会检出到本地分支并将远程分支拉入本地分支,这会创建冲突的文件,然后我进行编辑以修复提交,然后推送。其中仅显示已提交的本地文件,以及在本地分支中更改的文件作为来自合并的新更改。

git checkout local_branch
git pull origin remote_branch

但是在这个 repo 中,当发生合并冲突并将远程分支拉入本地分支时,它将所有远程更改带到本地 repos 并显示所有这些在本地分支中的更改(当它不是这个本地分支完成的,他们是因为远程分支拉进来的)。即使当我尝试 cherrypick 并提交本地分支更改时,它也会显示一个名为

的错误
git commit -m "merge conflict resolve1" src/partials/modal_content.html.twig
fatal: cannot do a partial commit during a merge.

有谁知道,我怎样才能将本地更改提交为合并?因为提交远程分支中的所有更改是不正确的,因为这个本地分支,当它不是时。

是不是合并设置快进了?

git merge --ff-only
git merge --no-ff

还是别的?

  • 解决冲突步骤:
  1. 编辑源文件,解决冲突
  2. git 添加已解析的文件。
  3. git 提交。
  4. git 推送到远程仓库

我不完全清楚你的心智模型 Git 正在做什么(相对于 Git 实际上 正在做什么,这不是match) 在这里出错了。这使得很难说出你知道 "just ain't so" 是什么东西。但是,我们当然可以回答这部分:

Is it because of a settings for the merge like fast-forward ?

没有

此处的几个问题之一是短语 remote b运行ch。这句话没有任何意义,因为不同的人用它来表示不同的东西,最后,没人知道对方说“remote b运行是什么意思]ch”。所以最好完全避免使用这个短语。让我们看看这两个命令:

git checkout local_branch
git pull origin remote_branch

第一个是比较直接的命令,即便如此,它实际上也不是完全直接的。当你 运行 这个时,Git 将:

  1. 尝试使用名称 local_branch 作为 b运行ch 名称.

    要成功,该名称必须已经存在于以 refs/heads/ 开头的 Git refs 集合中(您的 b运行ch 名称)。例如,如果您已经将该名称作为本地 b运行ch 名称,git rev-parse refs/heads/local_branch 将成功并生成提交哈希 ID。

    如果这确实有效,Git 将尝试检查此 b运行ch(与 more-limited git switch 命令在给定 b运行ch 名称,如果你有 Git 2.23 或更高版本:在 Git 2.23 中,复杂的 git checkout 命令被分解为两个更简单的 / more-limited 命令)。

  2. 如果名称 作为 b运行ch 名称存在,git checkout 将执行以下操作之一:它可以将 local_branch 视为 pathspec,并执行 Git 2.23 或更高版本将对 git restore 执行的操作;或者它可以尝试查找 remote-tracking 名称,例如 origin/local_branchcreate a new b运行ch 名称使用 remote-tracking 名称作为起点提交。 branch-name-creation 在内部被称为“DWIM”,Do What I Mean。在现代 Git 中,您可以使用 --no-guess 显式 禁用 此模式,并且您有一些额外的控制旋钮,用于它执行其他不希望的事情的情况。较旧的 Git 版本缺少 --no-guess 和控制旋钮。

    如果 Git 能够创建一个新的本地 b运行ch 名称,我们将按照与步骤 1 相同的方式继续检查它。

    如果git checkout表现得像git restore,这并没有改变哪个提交是当前提交,也没有改变哪个b运行ch是当前b运行ch,但它可能已经破坏了各种文件中未保存的工作。

结账失败当然有多种原因。假设它没有,并且 local_branch 现在存在(可能通过第 2 步),当前的 b运行ch name 现在是 local_branch 并且当前的 commit 现在是由名称 local_branch 选择的提交。如果索引和您的 work-tree 之前是“干净的”(从某种意义上说 git status 没有产生有关要提交的文件的消息 and/or 不提交),当前提交,Git 的索引,你的工作树也应该匹配。

git pull 命令要复杂得多。它首先是 运行s git fetch,这并不太棘手:

git pull origin remote_branch

运行s git fetch origin remote_branch。这让您的 Git 使用存储在 remote.origin.url 下的 URL 调用其他一些 Git;其他 Git 应该理解这个名字 remote_branch。另一个 Git 会将该名称解析为提交哈希 ID,假设它实际上是另一个 Git 中的 b运行ch 名称。另一个 Git 将发送您的 Git 该提交的哈希 ID。如果您的 Git 缺少该提交,您的 Git 将向其他 Git 请求该提交,以及该提交所需的 of 的任何父提交获取历史记录——一组提交——填补你这边的任何空白。

最终结果是,如果需要,您将获得提交。它们现在可以在您的 存储库和其他存储库中使用。如果它们已经可用,则此处提供的数据量相对较小 t运行;如果不是,则提供了足够的数据 运行 供您 Git 构建它们,因此现在您拥有了它们,包括 in 中的所有文件这些提交。

假设你有一个现代的 Git 并且 remote_branch 是 b运行ch on 的名称 Git ,并且您有一个标准的 remote.origin.fetch 配置,git fetch 操作现在将更新您的 remote-tracking 名称 origin/remote_branch,方法是存储到当他们的 Git 将名称 remote_branch 转换为提交哈希 ID 时,您的 Git 从他们的 Git 获得了相同的哈希 ID。

在所有情况下——无论你的 Git 是现代的还是古代的——你的 git fetch 最后将此特定提交的哈希 ID 写入 FETCH_HEAD 文件中的 Git 存储库(.git/FETCH_HEAD)。这是 second 命令 git pull运行s 将获取哈希 ID。当然,这个哈希 ID 与 origin/remote_branch 中的匹配(无论如何在现代 Git 中),因此您可以将第二个命令视为使用 origin/remote_branch:效果是相同的。

现在您已经在本地完成了所有 提交 git pull 运行 是它的第二个命令。第二个命令由您决定:您可以指定 Git 应该使用 git rebase,或者您可以让 Git 默认为 git merge。如果您不使用 git pull 的选项,则可以在 per-branch-name 的基础上执行此操作,而在这里,您没有使用任何选项。但我假设您将 pull 命令 运行 git merge 作为第二个命令。

(Rebase实际上是一系列cherry-pick操作,而each cherry-pick实际上是合并,所以当使用rebase时,你得到N 合并而不是 1 合并,在这里,其中 N 是你的 b运行ch 上的提交数,这些提交无法从哈希 ID 现在在 .git/FETCH_HEAD 或 [=49= 中的提交访问]. 如果你有 git pull 运行 git merge, 然后 配置选项如 merge.ff = onlymerge.ff = no, 或 command-line 选项,如 --no-ff--ff-only,很重要。)

设置合并

我们需要上述所有设置才能讨论 git merge 的工作原理。事实上,我们甚至需要更多一点,也就是说:Git 根据 commits 进行交易,而 Git 实际上 makes 来自 Git 调用的新提交,不同的是,它的 index,或者它的 staging area,或者——很少这些天 — 它的 缓存 。这个索引的存在,以及当时其中的某些内容,是为什么你得到这个错误的一部分:

fatal: cannot do a partial commit during a merge.

(我们不会在这里涵盖全部原因)。

一个提交,在 Git:

  • 由 random-looking 哈希 ID 编号;
  • 包含两部分:
    • 所有文件Git的快照;和
    • 一些元数据,或有关提交本身的信息,包括提交者、时间等。

元数据包括提交的 父项或父项 的原始哈希 ID。对于普通提交,只有一个父项:这是在本次提交之前的提交。

只要某物——例如 b运行ch 名称或提交——持有某个提交的哈希 ID,我们就说该东西——b运行ch 名称或提交—指向 something 持有其哈希 ID 的提交。这意味着 b运行ch name,如 masterlocal_branch 指向 提交,并且每个普通提交也 指向 其他一些较早的提交。

结果是我们得到了一个很好的简单线性链,尽管它是倒退的。我们可以把它画出来,右边是 newest 提交。我们将使用单个大写字母代表原始提交哈希 ID,因为它们看起来 运行dom。我们得到这样一张图:

... <-F <-G <-H   <--branch

b运行ch name,在 far-right,向后指向 last 提交提交链。该提交——具有哈希 ID H 的提交(实际哈希 ID 为 160 位,目前表示为 40 个十六进制字符)——其中包含一个快照和一些元数据。快照保存所有文件,一直冻结,就像提交时一样。元数据告诉我们是谁进行了提交,何时他们进行了提交,以及为什么(他们的日志消息)。 H 中的元数据还包含早期提交的原始哈希 ID G

Git可以使用H中的这个元数据来找到提交G,而提交G当然有一个每个文件的快照。所以 Git 可以 比较 G 中的快照和 H 中的快照。当两个文件匹配时,Git 可以说创建 H 的人单独留下了那个文件。在它们不同的地方,Git 可以声称制作 H 的人更改了 该文件。它也可以将 G 中的文件内容与 H 中的文件内容进行比较,并以差异的形式告诉我们 哪些行 发生了变化:说明如果应用于 G 中的 file-snapshot,将在 H 中生成 file-snapshot。

因此,这个 backwards-looking 提交链让我们:

  • 提取提交 H,将其用于实际工作;或
  • 比较 HG,看看 H 有什么变化。

但它也让我们HG。提交 G 现在有一个快照和元数据。我们可以告诉谁 做出了 提交 G,以及何时以及为什么(日志消息)。 Git 可以从 G 退一步回到 F,比较快照并告诉我们 G 中发生了什么变化 。当然,Git 可以返回到 F,并告诉我们谁制作了 F,并向我们展示发生了什么变化,然后从那里返回,依此类推,直到永远——或者直到链 运行 退出,因为没有 更早的 提交。

这就是我们在 运行 git log 时看到的内容,例如:最新 提交的提交元数据,以及我们要求的补丁一;然后是 previous 提交的提交元数据,如果我们要求的话,还有一个补丁。重复直到累或完成。

哪个提交是最新的提交?好吧,那是在 b运行ch name.

在继续合并之前,让我们再看一件事。假设我们有这个:

...--G--H   <-- main

我们现在创建另一个 b运行ch 名称,例如 dev 用于开发。我们必须选择一些 existing 将这个新名称提交给 point-to。显而易见的选择是最新的提交:

...--G--H   <-- dev, main

现在我们需要一种方法来知道我们正在使用哪个名称。因此,我们将特殊名称 HEAD 附加到这两个 b运行ch 名称之一。这告诉我们正在使用哪个 b运行ch 名称,这告诉我们正在使用哪个 commit:

...--G--H   <-- dev (HEAD), main

(请注意所有这些提交是如何在 both b运行ches 上进行的。)

如果我们进行 new 提交,Git 将写出一个新的快照(从它的索引,我们现在不会进入)并添加元数据:我们的名字等,以及当前提交哈希IDH作为父项,以便新提交指向回H。完成我们的新提交——例如获得一些新的哈希 ID I——Git 现在将新提交的哈希 ID 写入 current b运行ch 名称:

          I   <-- dev (HEAD)
         /
...--G--H   <-- main

这就是我们向 b运行ch 添加新提交的方式,一次一个。也许我们添加两个提交并得到:

          I--J   <-- dev (HEAD)
         /
...--G--H   <-- main

与此同时,其他人在 他们的 Git 中添加了 H 之后的两个提交,并将它们推送到某个托管位置的共享存储库(GitHub、Bitbucket、企业服务器等等)。所以我们 运行 git fetch 得到 他们的 新提交,在 他们的 b运行ch fred,或者他们怎么称呼它:

          I--J   <-- dev (HEAD)
         /
...--G--H   <-- main
         \
          K--L   <-- origin/fred

请注意,我们发现提交 L他们的 是最新的,在他们的 fred b运行ch 上—使用 我们的记忆他们的b运行ch,origin/fred。例如,当我们使用 git fetch 从 获取提交 时,我们的 Git 会更新我们对他们的记忆。

我们现在终于可以 运行 git merge。请注意,即使我们没有从他们那里得到提交K-L,我们也可以这样做。例如,我们可以制作我们自己的 b运行ch (barney?wilma?pebbles?) 指向我们的提交 H,然后制作在我们自己的存储库中有两个新的提交 K-L,但从未转到 origin。这里的关键部分是我们已经决定合并 JL

合并的工作原理

我们选择一些提交作为当前提交。如果我们想要提交 H,我们可以 运行 git checkout main。如果我们想要提交 J,我们 运行 git checkout dev。如果我们已经 on dev——HEAD 已经附加到名称 dev——那么这什么都不做,它只是说“already on dev”。如果我们在 main,这会让我们在 dev,所以 J 是我们当前的提交,通过名称 dev。无论如何我们现在有:

          I--J   <-- dev (HEAD)
         /
...--G--H
         \
          K--L   <-- origin/fred

我把名字 main 去掉了,因为我们不需要它而且它会妨碍我们。 (请注意,您可以随时添加或删除 b运行ch 名称。它们只是 查找 某些特定提交的工具,以便我们可以检查它, 并添加到从那一点开始的提交。但是请注意,由于哈希 ID random-looking 且不可预测,我们经常 需要 一个名称才能找到它最后一次提交。所以你只想 delete 一个不再需要的名字。删除名字不会影响提交——它仍然存在!——但它可能会使你不可能找到它。)

我们现在运行git merge origin/fred,或者git合并<em>hash-of-commit-L</em> ,或任何我们喜欢的告诉Git:使用提交L开始合并的过程。 commit 在这里很重要。我们使用的名称根本不重要。1

Git 现在所做的是找到一些 third 提交。第三次提交是 合并基础 。合并基础的技术描述是它是要合并的两个提交的最低公共祖先:我们当前的提交 J,我们称之为 ours,我们的提交 L我们从他们那里得到的,我们称之为 theirs。这利用了 提交图 这一事实——我们还没有定义;我们刚刚画了一些例子——是 DAG, applying the LCA-of-DAG algorithm。不过,描述这一点的简短方法是 Git 找到最好的 shaed 提交。在我们的例子中,那是提交 H:从目测中可以明显看出提交 H 在两个 b运行 上,并且它比提交 G 更好,因为它更靠近右侧:“较新”,即使提交日期不知何故搞砸了。2

无论如何,找到了——假设只有一个3——合并基地,Git现在需要弄清楚变化 . 合并的目标是合并更改,但提交不存储更改。这给我们——或者至少 Git——留下了一个问题:我们如何将一个快照更改?

我们已经知道这个问题的答案 因为我们通读了上面的设置内容。 (你确实读过,对吧?)要将快照变成变化,我们需要Git 比较两个快照

我们可以比较相邻的快照,一次一个。我们可以比较 HI,然后比较 IJ。那会弄清楚“我们”改变了什么。但事实证明,将 H 直接与 J 进行比较,4 更容易,而且效率更高。这就是 Git 所做的。无论 Git 发现什么改变了,当比较合并基础 H 快照与当前提交 J 快照时,we 改变了什么。

接下来,Git 对合并基础和 theirs 提交做同样的事情 L:比较两个快照。无论改变什么,这就是他们改变的。

请注意,这些更改列表是在 per-file 的基础上进行的。如果文件 f1.ext 存在于所有三个提交中,并且 f2.ext 存在于所有三个提交中,则 f1-in-base-vs-each-commit 有一组 ours-and-theirs 更改,还有一组设置为 f2-in-base-vs-each-commit,依此类推。对于 totally-new 文件和重命名的文件,此规则有一些例外,但一般来说,我们按 file-name.

配对

当对一个特定文件的两个更改“重叠”时,您会遇到 合并冲突。假设在 f1 中,我们将 red ball 更改为 blue ball,并且在同一行中,他们将 red ball 更改为 red bat。 Git 在进行合并时在 line-by-line 基础上工作,不知道是否应该采用 our 行或 their 线。正确的答案可能是两者都选,或者两者都不选。 Git 只遵守非常简单的规则:如果我们触及了一条线而他们没有,就拿走我们的。如果他们触及了一条线而我们没有,那就拿走他们的。如果我们都触及了一条线,则声明冲突。

有一些方法可以稍微修改这些规则,但总的来说,就是这样。如果您看到合并冲突,那只是因为 Git 正在将某些合并基础中的内容与每个选定提交中的内容进行比较,并发现它无法自行合并的更改。


1默认情况下,Git 将生成一条(低质量)合并消息阅读 merge b运行ch <em>name</em> 从名字。所以名字有点重要。但是,如果您用更高质量的合并消息替换它,它就不再重要了。请注意,git pull 使用 -m 选项,以便它可以传递哈希 ID,同时将默认消息更改为 merge b运行ch '<em>name</em>' 的 '<em>url</em>'。这是使用 git pull 的原因之一,因为此消息的质量可能略高。不过,它的质量仍然不是很高:任何自动生成的消息都不可能。再者,人们通常不会查看合并消息。在紧握的手上,也许人们不看合并消息因为它们质量低因为它们通常是自动生成的。

2请注意,每个提交都有 两个 日期与之关联。一个是作者日期,另一个是提交者日期。这些日期不可信任,因为生成它们的计算机时钟可能未正确同步并且它们可能被欺骗。 Git 有时使用它们来显示提交,但不用于查找合并基础:合并基础仅由 graph.

确定

3有些图,例如上面链接的维基百科页面上的图,有多个合并基础。 Git 使用它所谓的 递归 合并策略来处理这种情况。我们将在这里跳过所有这些细节。

4在某些情况下,主要涉及文件重命名, commit-by-commit 会很有成效。也许有一天 Git 可以做到这一点。


完成冲突合并的秘诀

处理冲突合并时:

  • 有三个输入文件可用:合并基础版本、您的提交版本和他们的提交版本。请注意,这些都是来自 frozen-for-all-time 提交,所以你返回提交以获取它们。但是,有一个获取它们的捷径,我们稍后会看到。

  • 文件的 工作树 副本——您可以查看和编辑的副本——将 Git 尽最大努力将两者结合起来套的变化。在某些情况下,Git 会成功合并一些更改,而留下其他有合并冲突的部分。 Git 用“冲突标记”包围冲突地区。

  • 如果你设置,git configmerge.conflictStylediff3——默认样式叫做merge——你会得到三个 部分在冲突地区,而不是只有两个。第三部分位于 <<<<<<<“我们的”行和 >>>>>>>“他们的”行之间,显示了 merge 中的内容文件的基本 版本。我发现这种冲突优于 far 优于 merge 样式,它省略了 merge-base 副本。

要获得 四个 文件,您可以在编辑器中 on/with 工作,Git 提供了 git mergetool 命令。这个工作的方式有点复杂,需要我们再次提到索引 / staging-area。

通常情况下,索引保存每个文件的一份副本。这些“副本”——采用 Git 在提交中使用的内部形式,这意味着它们是 de-duplicated 因此通常不会 space存储——Git 在进行新提交时使用的。 Git 实际上并没有使用您可以看到和处理/使用的文件:该文件是普通的日常文件格式,因此程序可以实际看到它并使用它。 Git 的提交文件采用特殊的 Git-only、压缩和 de-duplicated 形式,因此它们占用的 space 比简单的快照系统要少得多。

git commit 命令仅通过(非常快速地)扫描索引来创建新的快照。这列出了准备好的 ready-to-go 文件的内部哈希 ID。我们这些使用旧版本控制系统的人,必须通读 工作树 文件副本,通常会对 git commit 运行 的速度感到惊讶:什么,你是说我 运行 git commit 并且 现在 发生了?我本来要去吃午饭的!

总之,对运行git merge、Git索引。现在,它不再保存每个文件的 一个 副本,而是保存 三个: 一个来自合并基础,一个来自“我们的”提交,以及一个来自“他们的”承诺。 Git 可以很快地消除大量工作,因为在一个大项目中,很可能许多文件在所有三个提交中都是 100% 相同的。这些是没有人更改的文件。 Git 可以立即将那些折叠成一个文件的一个副本。也有可能在某些地方我们更改了一些文件而他们根本没有碰它,反之亦然。这里再次 Git 可以立即(通过 de-duplicating 哈希 ID)判断是这种情况,并且只获取更新的文件,折叠其他文件。剩下的就是那些可能冲突的文件,因为我们更改了文件并且他们更改了文件。

对于这些情况,Git 现在 运行 在每个文件上使用 低级别 合并驱动程序。如上所述,内置的发现更改的线路:如果我们触及某条线路而他们没有,反之亦然,该驱动程序可以合并这些更改。它这样做并将其写出到工作树副本。如果存在重叠更改或邻接更改,例如,我们和他们都在某些行之间或文件末尾添加,并且 Git 不知道 哪个顺序 在此处使用—Git 声明冲突,并将冲突行写入工作树副本。

因此,当git merge因冲突而停止时,我们有驱动程序试图合并工作树副本中的文件,而其他三个副本在Git的索引中。这些有 阶段编号: 阶段 1 = 合并基地,2 = 我们的,3 = 他们的。

git mergetool

git mergetool所做的是:

  • 对于每个冲突文件,提取三个索引副本,然后运行一些命令——你选择的合并工具——对四个文件:三个输入,以及 Git 合并它们的尝试。

  • 该命令完成后,清理多余的文件。使用该工具的“这是正确的合并结果”文件来完成该文件的合并。这个 运行s git add 在合并的工作树副本上,它从索引中删除三个副本,并放入一个副本(在“阶段零”)以标记文件已解决。

  • 一些工具被认为是“好”的:它们的退出代码告诉 mergetool 文件是否真的被解析了。有些工具未知,git mergetool 将作为你:解决了吗?有时 Git 会将工具的输出与工具的输入进行比较,以 猜测 工具是否有效。这有点烦人,所以如果 git mergetool 不能很好地与您自己喜欢的工具一起使用,您可以克隆 Git 源代码,添加一些东西来处理 您的 工具,并提交更新。

手工做事

merge.conflictStyle设置为diff3,我只是在我的编辑器中打开冲突的文件(vi / vim;其他人如emacs,或其他编辑器:我以前使用emacs回到过去糟糕的日子,当时 vi 无法进行适当的窗口化)。我把它修好然后写回去,然后 运行 git add 结果。

如果查看文件的三个输入版本之一中的内容真的很重要 — 有时 Git 尝试合并内容的尝试非常糟糕并且读取文件很混乱 — 有一些简单的方法获取它们:

  • git show 接受语法 :<em>number</em>:<em>file</em>,例如,git show :1:foo.cgit show :2:foo.c,等等。数字是 1=基地,2=我们的,3=他们的。例如,使用 git show :1:foo.c > foo.c.base 是获取文件基本版本以便在编辑器中查看的快速方法。

  • git checkout(或 Git 2.23+ 中的 git restore)允许您用我们或他们的版本覆盖工作树副本,--ours--theirs 选项。 (没有 --base 选项,这很烦人。)

如果您不小心解析了文件

假设您正在编辑 foo.c,或者在上面使用了 mergetool。你把它写出来并使用 git add。现在您构建并测试程序,呃,那是错误的代码:它不起作用。

您可以 re-create 合并与 git checkout -m(或 git restore -m,在 2.23+ 中再次发生冲突)。 请注意,这将消除您的合并尝试。您将从头开始。

合并带有覆盖的单个文件

如果您使用过 Mercurial,您可能知道它的合并命令提供了一种方法,可以对每个文件 执行 git merge -X oursgit merge -X theirs 的等效操作 基础。 Git 确实应该内置这个,但没有。但是,Git 是否具有上述 git show 技术。

只需 git show 将三个文件中的每一个提交到您的工作树中,使用能让您记住哪个是哪个的名称。然后运行git merge-file就三个文件。详见the documentation;请注意 git merge-file 有我们的、他们的和联合选项。

如果您不小心解析了该文件,您可以使用 git checkout -m(或 git restore -m)来撤消它。或者,您可以使用:

git show HEAD:path/to/file

和:

git show MERGE_HEAD:path/to/file

从特定提交中提取文件。不幸的是 Git 不会在任何地方保存合并基础哈希 ID,但您可以 运行:

git merge-base --all HEAD MERGE_HEAD

找到合并基的哈希 ID。理想情况下,这只会打印一个哈希 ID,之后您就可以开始了。 (如果它打印出不止一个散列 ID,则你有一个递归合并案例,需要喘口气。)