首页 > 解决方案 > Git 合并冲突(假设场景)

问题描述

嗨,我对使用 git 很陌生,但我想知道如何解决以下假设示例:

有两个人(A 和 B)为一个分支(主)上的远程仓库做出贡献。我们将假设 B 与 repo 是最新的,但 A 不是。A 刚刚修改了一行新代码,并且刚刚尝试推送到 repo,并被告知他需要拉取最新版本。拉动时,他遇到了合并冲突,并且能够解决它。完成后,A 成功推送到 repo。现在 B 意识到 A 的更新很重要,并决定他需要从 repo 中拉取,但他已经在本地进行了更改,所以他暂时只是 stash。在提取并应用存储之后,B 意识到他遇到了与 A 之前遇到的相同的合并冲突。

非常感谢您的帮助。

标签: gitgithub

解决方案


这种情况永远不会发生。原因是合并操作的输入之一已自动更改。但是,人 B(我将在下面称为 Bob)可以看到合并冲突。

请记住,在 Git 中merging是一个涉及提交的操作。也就是说,当我们进行合并时——动词形式,合并——我们会找到一些提交并完成我们的工作。还要注意,每个提交都代表一个完整的快照:所有文件,在特定提交时及时冻结。如果您想查看您的文件在昨天、去年或其他什么情况下的样子,它们就在当天提交的提交中。

合并操作有三个输入!我们作为运行 Git 的人,直接指定其中之一

$ git merge origin/develop

例如。该名称origin/develop解析为某个特定的提交 ID——一个大而丑陋的哈希 ID 字符串,例如5d826e972970a784bd7a7bdf587512510097b8c7,这是我们在存储库中的一个提交,我们之前获得的,可能是通过运行git fetch. (请记住,这git pull本质上是一个方便的命令,意思是:为我运行git fetch,然后一旦完成,运行第二个 Git 命令,通常是git merge,为我。)

合并的第二个输入是当前提交是什么。也就是说,在我们运行之前git merge(可能通过git pull),我们运行了一个git checkout命令:

$ git checkout develop

这会选择分支上的最新提交,因为分支名称的定义“分支上的最新提交”。这意味着分支名称会随着时间的推移不断变化。要查看master现在是哪个提交,或者现在develop是哪个提交,您可以运行git rev-parse masteror git rev-parse develop—that 将吐出他当前的哈希 ID。稍后再次运行,当有更多提交时,您将获得不同的哈希 ID。

绘制历史

因此,考虑到这一点,绘制提交图总是值得的。提交图存储库中的历史,因为 Git 中的历史只不过是提交。

如果您之前没有绘制提交图,这需要一些练习。有很多方法可以在浏览器或 GUI 中绘制或查看它们,尽管我对 GUI 有一种健康的不信任,因为它们中的很多都在撒谎(通常有充分的理由与表现良好有关,但仍然是恼人的)。有很多方法可以做到这一点,但我喜欢对 StackOverflow 帖子横向进行:

          o--o--o   <-- develop (HEAD)
         /
...--o--*
         \
          o--o   <-- origin/develop

在这些图中,最近的提交位于右侧,时间向后移向左侧。分支名称指向最右边(最新)的提交,因为根据定义,名称是分支中的最后一个提交。我们——和 Git——必须从头开始,然后回到更早的提交,看看事情进展如何。

从这张图中,我们可以看到我们的develop三个提交——圆形o节点——不在共享历史上,而我们origin/develop的两个提交不在共享历史上。

共享历史从提交开始,*并在时间上向后继续。*在此图中,Commit是提交developorigin/develop. 当 Git 进行真正的合并时——执行合并过程的合并部分——<strong>三个输入是合并基础、HEAD提交 ( --ours) 和另一个提交 ( --theirs)。

Git 进行合并的方式现在实际上很容易看到。因为每个提交都是一个完整的快照,Git 首先将合并基础快照提取到一个临时区域。然后它提取我们的提交——嗯,它已经存在了,真的——和他们的提交,现在它运行两个单独的比较,我们可以自己使用一次复制一个git diff。我们首先找到提交的哈希 ID *(也许通过查看图表),然后运行:

git diff --find-renames <hash-of-*> HEAD             # what we changed
git diff --find-renames <hash-of-*> origin/develop   # what they changed

现在git merge要做的就是结合这些变化。从基础文件开始。然后,无论我们在哪里更改了文件,如果他们没有更改该文件,请使用我们的。无论他们在哪里更改了文件而我们没有,都使用他们的。无论我们哪里更改同一个文件,合并更改。

如果 / 当两个变更集的组合尝试更改相同文件的相同行时,就会发生合并冲突。在这种情况下,Git 会保留文件的所有三个原始副本(隐藏在 Git 的index中),并尽最大努力将更改以及一些冲突标记组合到工作树中。

如果 Git 根本没有遇到任何冲突,Git 就会进行新的提交。或者,在您手动修复了 Git 在发生冲突时留下的混乱之后,可以对结果进行最终提交。这个提交在我们的图表中看起来有点奇怪,因为它之前有两次提交,而不是通常的一次。我会给这个新的提交一个字母ID(不是真正的哈希ID,只是一个替身)M,:

          o--o--o--M   <-- develop (HEAD)
         /        /
...--o--*        /
         \      /
          o----o   <-- origin/develop

这种提交是一个合并,或作为名词合并:一个双亲提交,两个父母是旧的HEAD(父母#1)和另一个提交(#2)。

现在假设你的人 A (Alice) 和 B (Bob) 继续工作

您建议 Alice 进行了合并然后运行git push。当 Alice 这样做时,假设她git push成功了,那将获得另一个 Git 存储库来调用M develop. 也许这第三个存储库位于 GitHub 上,例如:

          o--o--o
         /       \
...--o--o         M   <-- develop (HEAD)
         \       /
          o-----L

我画的有点不同,但它是相同的一组提交。这里没有,只是导致两个先前提交origin/develop的合并提交。M我还取消了旧的共享点的标记,并L为 Bob目前的提交写了一封信,因为我们很快就需要讨论它。

现在 Bobgit fetch他的存储库中运行,以获取 Alice 的工作。虽然 Bob 还没有自己的新提交,但他获得了新的提交。请注意,他develop已经存在并且当前指向 commit L

          o--o--o
         /       \
...--o--o         M   <-- origin/develop
         \       /
          o-----L   <-- develop (HEAD)

请注意,鲍勃的develop分支是他的分支!如果他有未提交的更改,他可以运行git stash以将它们保存在一个提交中——实际上是两个提交,但我们现在假设只有一个——那是​​在任何分支上:

          o--o--o
         /       \
...--o--o         M   <-- origin/develop
         \       /
          o-----L   <-- develop (HEAD)
                 \
                  S   [stash]

现在 Bob 可以运行git merge --ff-only(或者git pull,它将执行另一个什么都不做的提取,因为没有什么比 更新M,然后执行快进非真正合并“合并”)以使Bobdevelop点提交M

          o--o--o
         /       \
...--o--o         M   <-- develop (HEAD), origin/develop
         \       /
          o-----L
                 \
                  S   [stash]

Bob 现在可以应用并删除(或“弹出”)存储以将更改放入他的工作树中。如果这可行,他们将准备好添加和提交。然后 Bob 进行新的提交,我们可以将其绘制为N

          o--o--o
         /       \
...--o--o         M--N   <-- develop (HEAD), origin/develop
         \       /
          o-----L

的内容N已经包含M, 因为git stash applytake的内容S,将其与其父级进行了比较,然后对中的任何内容进行了相同的更改M

当 Bob 可以看到冲突时

Bob 可以看到冲突的点不是在获取和快速转发期间,而是在 Bob 运行时git stash apply(或git stash pop以运行开始git stash apply)。实际上,这运行了另一个合并!但它运行的合并不会合并提交结束。这只会合并动作的动词部分。

此合并将提交S作为其最后一个“其他提交”输入。中间的输入是HEAD——commit——M和往常一样是当前的提交,第一个输入是两者的合并基数,即 commit L

          o--o--o
         /       \
...--o--o         M   <-- develop (HEAD), origin/develop
         \       /
          o-----L
                 \
                  S   [stash]

Git 比较(差异)LM找出“我们”做了什么。(当然,这实际上是爱丽丝所做的,或多或少,但 Git 不在乎。)它比较LS“他们”(真的,鲍勃)做了什么。然后它结合或尝试结合这两个变更集,就像常规合并一样。

如果一切顺利,就git stash到此为止。还没有新的提交。如果进展不顺利,git stash则以合并冲突停止,让 Bob 解决合并冲突。Bob 完成后,Bob 的下一次提交将是普通提交,而不是合并提交。

如果 Bob 使用git stash pop并且存在合并冲突,Git 在该apply步骤之后停止,并且 Bob 仍然有存储:他必须git stash drop在修复混乱后运行以丢弃它。但是如果 Git 认为合并进行得很顺利,并且 Bob 运行了git stash popGit将为git stash dropBob 运行。所以必须小心一点git stash:有时它会同时进行应用和删除,但有时它会在应用后失败并且不执行删除部分。

(我主要建议避免git stash使用它,但如果您要使用它,请分别执行 apply 和 drop,以避免在Git认为是干净的方式应用时意外丢弃存储,但实际上是您的错误。这真的确实会发生,即使对于 Git 专家也是如此,并且从中恢复是很痛苦的。)


推荐阅读