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
还是别的?
- 解决冲突步骤:
- 编辑源文件,解决冲突
- git 添加已解析的文件。
- git 提交。
- 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 将:
尝试使用名称 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 命令)。
如果名称 不 作为 b运行ch 名称存在,git checkout
将执行以下操作之一:它可以将 local_branch
视为 pathspec,并执行 Git 2.23 或更高版本将对 git restore
执行的操作;或者它可以尝试查找 remote-tracking 名称,例如 origin/local_branch
和 create 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 = only
或 merge.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,如 master
或 local_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
,将其用于实际工作;或
- 比较
H
与 G
,看看 H
有什么变化。
但它也让我们从H
到G
。提交 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
。这里的关键部分是我们已经决定合并 J
和 L
。
合并的工作原理
我们选择一些提交作为当前提交。如果我们想要提交 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 比较两个快照。
我们可以比较相邻的快照,一次一个。我们可以比较 H
与 I
,然后比较 I
与 J
。那会弄清楚“我们”改变了什么。但事实证明,将 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 config
,merge.conflictStyle
到diff3
——默认样式叫做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.c
、git 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 ours
或 git 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,则你有一个递归合并案例,需要喘口气。)
我有一个奇怪的合并冲突问题,我不确定为什么它只发生在某些存储库上,(可能是由于合并规则上的回购设置)。
通常,如果我有合并冲突,我会检出到本地分支并将远程分支拉入本地分支,这会创建冲突的文件,然后我进行编辑以修复提交,然后推送。其中仅显示已提交的本地文件,以及在本地分支中更改的文件作为来自合并的新更改。
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
还是别的?
- 解决冲突步骤:
- 编辑源文件,解决冲突
- git 添加已解析的文件。
- git 提交。
- 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 将:
尝试使用名称
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 命令)。如果名称 不 作为 b运行ch 名称存在,
git checkout
将执行以下操作之一:它可以将local_branch
视为 pathspec,并执行 Git 2.23 或更高版本将对git restore
执行的操作;或者它可以尝试查找 remote-tracking 名称,例如origin/local_branch
和 create 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 = only
或 merge.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,如 master
或 local_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
,将其用于实际工作;或 - 比较
H
与G
,看看H
有什么变化。
但它也让我们从H
到G
。提交 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
。这里的关键部分是我们已经决定合并 J
和 L
。
合并的工作原理
我们选择一些提交作为当前提交。如果我们想要提交 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 比较两个快照。
我们可以比较相邻的快照,一次一个。我们可以比较 H
与 I
,然后比较 I
与 J
。那会弄清楚“我们”改变了什么。但事实证明,将 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 config
,merge.conflictStyle
到diff3
——默认样式叫做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.c
、git 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 ours
或 git 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,则你有一个递归合并案例,需要喘口气。)