繁体   English   中英

当我在另一个本地分支上重新设置本地分支以从其历史记录中排除提交时,为什么会给 Git 带来合并冲突?

[英]Why gives Git a merge conflict, when I rebase a local branch on another local branch to exclude a commit from its history?

创建了一个分支dummy对象。 新分支上的第一次提交会添加一个文件a.py 在下一次提交中,变量aa.py的第一行中被赋值为1 接下来的四次提交将递增的值分配给a 最后创建第二个分支dummy2指向最后一次提交。 dummydummy2的提交历史如下所示:

在此处输入图像描述

让我们假设在第二次提交( a = 1 )中做了一些应该包含在dummy历史中但从dummy2历史中排除的事情。 实现此目的的一种方法是将dummy2重新设置在a=2上。 这样做的语法是:

git rebase --onto ecd5f3fe 718f88d8 dummy2

Git 现在应该将提交a=2复制到a=5并将它们附加到提交added a file中。 但是,使用这种方法我们会遇到合并冲突:

CONFLICT (content): Merge conflict in my_scripts/a.py
error: could not apply 8a2a26b... a = 2
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 8a2a26b... a = 2

合并冲突 UI 告诉我们,git 不确定 rebase commit a=2 on added a file

在此处输入图像描述

我们可以告诉 git 在像这样进行 rebase 时总是更喜欢我们当前的分支

git rebase -Xtheirs --onto ecd5f3fe 718f88d8 dummy2

这会自动为我们解决合并冲突并给出想要的结果

![在此处输入图像描述

但是,为什么git首先不确定呢? 为什么它不知道在这种情况下a=2是通往 go 的方式?

Git 在这里混淆的原因很简单,但在我们到达那里之前,让我在这里解决一个危险信号:

合并冲突 UI...

Git没有合并冲突 UI。 1这导致很多人添加不同的方法,每个人都有不同的方法来表达冲突。 知道 Git 本身在这里只提供两件事会很有帮助:

  • 首先,Git 提供所有三个输入文件 我们稍后再看这个。 这三个文件都是不可见的:你看不到它们中的任何一个。 您必须让 Git 为您提取它们。 人们添加的各种 UI 通常会这样做,但是,请参阅“不同的呈现方法”。

  • 最后,Git 还在您的工作树中提供了自己的最大努力,并用冲突标记进行了标记。 默认的冲突标记样式称为merge样式,它显示来自三个输入文件中的两个的行。 另一种可用的冲突标记样式称为diff3 ,它显示三个输入文件中所有三个的行。 我发现这种风格更胜一筹,尽管您仍然需要了解三个输入文件。


1您可以将 Git 提供的 UI 称为这两件事,但是……这让我们回到了亚伯拉罕·林肯的名言 引用说林肯问一条狗有多少条腿,如果你称尾巴为腿。 显而易见的答案是 5,对此的回答是:“啊,但称尾巴为腿并不能使它成为一体。” 根据这里的链接,林肯确实说过这句话,但是关于小牛,而不是狗。


到底发生了什么

git rebase命令在原则上有时实际上是通过重复调用git cherry-pick Git 的git-cherry-pick文档用一句话描述了cherry-pick:

给定一个或多个现有提交,应用每个引入的更改,并为每个提交记录一个新的提交。

(变基代码一次执行一个,以进行额外控制,因此我们可以忽略“或更多”部分。)“应用更改......并进行新的提交”对我来说意味着复制一些现有的提交。 这里缺少的——但强烈暗示——这个“应用更改”实际上是一个合并操作:

...有关解决此类冲突的一些提示,请参阅git-merge(1)

一个合并操作涉及三个提交。 那么:这里的三个提交是什么? 嗯,一个很明显:我们必须告诉git cherry-pick我们希望它复制哪个提交,如下所示:

git cherry-pick a123456

复制其 hash ID 为a123456的提交。

现在,如果我们使用git showgit log -p查看提交,我们通常会将提交视为一组更改,以及一些附加信息,例如提交的 Z0800FC577294C34E0B28AD2839 等。 但是没有提交实际存储更改; 相反,所有提交都存储快照元数据 快照是Git 在您或任何人进行提交时知道的每个文件的完整快照 元数据是 Git 保存提交的人的姓名以及他们提供的任何日志消息等内容的地方。

那么,我们如何才能从快照中获得一组变化——“有什么不同”呢? 答案很简单:我们并排放置两张快照,然后玩Spot the Difference游戏。 如果单击此链接,您将看到一些并排的快照。 我希望我不会说太多,在一对快照中,其中一个区别是猫从拿着勺子变成了拿着棒棒糖。

在这里,Git 承担了发现差异的工作。 然后它会打印出一个“diff hunks”列表供我们查看,上面写着:如果你拿左边的文件,并对它进行这些更改,你会得到右边文件中的内容。

因此,要将提交转换为更改,我们需要将提交的快照与一些较早的快照进行比较。 我们如何做到这一点?

好吧,每个提交不只是存储一个快照。 它还存储元数据:例如作者的姓名。 在元数据中,Git 添加了Git需要的东西:父提交 ID。 每个提交都有一个唯一的 ID 号,我们通常称之为hash ID Git 称其为object ID Git 曾经将其称为SHA-1 hash ,但 Git 人们正试图从 SHA-1 迁移到 SHA-256。 不管我们怎么称呼它,它都是一个以十六进制表示的数字,它允许宇宙中任何地方的任何 Git 找到提交,只要 Git 实际上具有提交,因为该数字对于该特定提交是唯一的。

If some commit whose hash ID is C (Commit or Child) holds the hash ID of its immediate predecessor commit—let's call that "commit P ", for Parent—then we say that commit C points to commit P . 事实上,几乎每个提交2都有一个父级,或者有时不止一个父级:

C1 <-C2 <-C3 ... <-Cn-1 <-Cn   <--branch

是一个简单的提交链,我们已经编号。 分支名称branch指向链中的最后一个,它指向倒数第二个,它指向更远的位置。 最终我们到达第一个提交,这里编号为1而不是正确的零3 ,因此n提交从1n (包括此处)计数。


2此规则的唯一例外是所谓的根提交:每个非空存储库都有一个,因为根据定义,我们在新的空存储库中进行的第一次提交是第一次提交,因此没有父提交. 可以将多个根提交放入存储库,但这需要一些额外的工作。 不过,一个是免费的,因为它是自然发生的。 没有理由需要额外的根提交:它们只是起点,这使它们成为历史的终结。 History ,在 Git 存储库中,只是提交。 因此,更早的历史是更早的提交,当你点击 / 根时,没有更早的东西。

3如何与一群数学或信息学人士开始争论:询问是否从零开始编号。


合并

你问的是樱桃采摘——嗯,是关于基的,但这意味着樱桃采摘——但首先从真正的合并开始是个好主意,因为在真正的合并中,这一切都更有意义。

当我们合并时,我们有两个分支。 这两个分支上的提交可能看起来有点像这样:

       o--...--I--J   <-- branch1 (HEAD)
      /
...--B
      \
       o--...--K--L   <-- branch2

较新的提交位于右侧,因此JL是这两个分支上的最后提交。 我们做了一个git checkout branch1git switch branch1来选择提交J来处理/使用。

提交J指向更早的提交I ,后者指向更多的提交,依此类推。 同时,提交L指向一些更早的提交K ,它指向更多的提交。

现在我们运行git merge branch2 这告诉 Git:看看我们的提交J和他们的提交L 通过历史追溯他们的父母联系。 Git 一直这样做,直到它到达提交B ,它位于两个分支上,因此充当合并基础提交。

这是合并过程的三个输入提交。 具有快照的提交B合并基础 提交J ,它有一个快照,是我们的提交。 提交L ,它有一个快照,是他们的提交。 提交B具有位于两个分支上的特殊属性,并且由于是两个分支上的最新提交,因此也比两个分支上的任何早期提交都“更好”。

git merge的工作是合并工作 为此,Git 必须找到更改 它可以使用这些快照来做到这一点。

一种方法是将B与顶行的子提交进行比较,然后将该提交与下一个提交进行比较,依此类推,直到我们到达IJ 但是对于 Git 来说,将B快照直接与J快照进行比较会更短、更快、更容易。 这种比较的结果是一堆 diff hunks:做这个,那个,以及提交B的另一件事,你将在提交J中获得快照。 这就是如何将B变成我们的提交。

类似地,将B中的快照与L中的快照进行比较就足够了。 这会产生一堆 diff hunks:执行 X、Y 和 Z 来提交B ,你会得到提交L 这就是如何将B变成他们的提交。

然后,要进行合并,Git 只需组合所有这些差异大块。 如果我们改变了一些文件而他们没有改变,那就是我们的改变。 如果他们更改了某些文件而我们没有更改,那将得到他们的更改。 如果我们更改了某个文件,那么……好吧,只要更改不重叠,Git 就可以进行这两项更改。 不过,有时这些更改会重叠,或者直接触及边缘。 对于重叠,有一个明显的问题,对于那些在边缘接触的变化——邻接的变化——有时会出现顺序问题,经验表明,自动组合这些并不能很好地工作。 这就是 Git 所做的:当它们重叠或邻接时,它无法组合更改 这是一个合并冲突

当没有合并冲突时, Git 能够从B获取文件,添加我们所做的任何更改以及他们所做的任何更改,并将该更新/组合更改文件用作建议的文件 go 在建议的合并提交M中。 发生合并冲突时,Git:

  1. 将来自BJL的所有三个输入文件保存在 Git 的索引中(又名staging area ,但这里的索引已被扩展,以便它可以容纳单个文件的三个副本)。 索引中的文件副本不是直接可见的,但是git status会说这个文件是unmerged

  2. 写入您的工作树文件(您可以在编辑器中查看并打开以进行更改的文件)Git 会尽最大努力合并该文件的更改。 这包括冲突更改周围的冲突标记。 正如我之前提到的,如果您告诉 Git 使用diff3样式,您将获得所有三组行(包括来自B原始行)。 4否则,您只会得到ours ( HEAD commit, J here) 行和theirs (commit L , here) 行。

如果你有git mergetool合并工具运行一些实际的 UI 工具,或者选择一些你喜欢的 UI 并使用它的合并工具,那么该合并工具可能会让你得到所有这些:所有三个输入文件Git 合并它们的尝试。 但是每个 UI 都是不同的,您必须检查您的UI 的任何文档以查看它在此处的行为方式。

在任何情况下,如果存在合并冲突,Git 将在合并中间停止,索引/暂存区域仍至少部分扩展(对于每个冲突文件)。 您现在的工作是清理混乱,即解决每个冲突的合并,但是您希望这样做。 要向 Git 发出信号表明文件的工作树副本现在包含正确的结果,您将运行git add on该文件。 这将删除三个输入并将索引/暂存区域副本中的内容(现在减少为单个文件副本)替换为工作树中内容的副本(但变成准备提交的内容:Git 将文件存储在提交中,制作快照,但不会将它们存储普通的日常文件:这将使 Git 存储库变得非常庞大且笨拙)。

合并的最终结果是一个合并提交:

       o--...--I--J
      /            \
...--B              M   <-- branch1 (HEAD)
      \            /
       o--...--K--L   <-- branch2

像每次提交一样,提交M存储一个快照。 它唯一的特殊之处是它添加了第二个父级L而不是一个父级J ,以记录 (a) 它实际上是一个合并,并且 (b) 合并将导致J的工作与工作相结合这导致了L 通过解决此处的任何冲突,您已向 Git 声明M中的快照包含正确的组合。 5


4为此, merge.conflictStyle配置为diff3 ,例如:

git config --global merge.conflictStyle diff3

如果您不喜欢这样,请将其配置为merge ,或删除diff3设置,因为默认设置已经是merge (目前没有其他 styles 可用。)

5确保提供正确的组合。 如果您不解决冲突,而只是git add工作树文件,那么您告诉 Git正确的解决方案是保留所有内容,包括冲突标记


采摘樱桃

在我们的樱桃挑选示例中,我们可能有两个或更多这样的分支:

       o--o--P--C--o--...--G   <-- branch1
      /
...--B
      \
       o--...--o--H   <-- branch2 (HEAD)

这很像我们的合并示例。 但是我们不想合并这两个分支 我们只想复制子提交C的效果。 我们希望 Git 将P中的快照(父级)与C中的快照进行比较。 无论这里有什么不同,这些都是我们希望 Git 添加到提交H的更改。

但是——举一个非常简单的例子——假设在PC之间,他们更改了文件xyz.py的第 10 行以修复注释中的拼写:

#! /usr/bin/env python
#
# ... big comment here ...

同时,在我们的提交H中,这不是第 10 行 我们的评论要么更长,要么更短。 假设拼写错误存在,但在第 4 行。

如果我们只有一个 diff hunk,我们可能会通过使用 diff hunk 的上下文来拯救我们。 但是 Git 可以做得比仅仅根据上下文猜测要好得多。 如果我们让 Git 运行git diff ,将P中的快照与我们的提交H中的快照进行比较怎么办?

这个diff 会说:删除从第 3 行到第 8 行的所有行。也就是说,它们的diff 中的第 10 行是什么——以修复拼写错误——对应于我们diff前面的六行,因为从快照P到 go快照H ,我们删除此点上方的六行。

这个额外的差异信息为 Git 提供了将“他们的”更改应用到“我们的”文件所需的准确信息。 所以 Git 这样做。 但是现在我们 go 来观察一些关于应用差异的数学特性:

  • 删除就像减去东西。
  • 添加就像添加东西。
  • 因此,如果我们获取快照P中的内容并将我们的更改应用于添加和删除内容,并将其与他们的更改相结合以添加和删除内容,我们将删除正确的内容,并添加正确的内容,以保留我们的更改并同时添加他们的更改

这意味着我们可以将提交P中的文件视为合并基础,并简单地使用现有的合并引擎,就像git merge一样。 所有这一切的唯一特别之处在于P而不是B是合并基础——当 Git 完成时,新的提交不应该是合并提交,而只是一个普通的提交:

       o--o--P--C--o--...--G   <-- branch1
      /
...--B
      \
       o--...--o--H--C'  <-- branch2 (HEAD)

我们的新提交HC'之间的区别应该与PC之间的区别相同,除了行号等 - 以及任何需要解决的合并冲突

一个具体的例子:你的情况

在您的情况下,您有一个提交8a2a26b... ,它会更改:

a = 1

(在其父提交中,无论 hash ID 是)到:

a = 2

您正在尝试在根本没有a =行的提交上挑选它。

Git 因此将8a2a26b...的父级与当前提交进行比较。 这个差异说:

-    a = 1

即,完全删除将 1 分配给a的行。 6那是你的改变:删除那行。

同时,Git 还将 8a2a26b... 的父级与8a2a26b...进行8a2a26b... 这个差异说:

-    a = 1
+    a = 2

即,将分配1的行更改a ,更改为将2分配给a的行。

Git 发生冲突,因为它无法将“删除行”与“更改行”结合起来。 使用diff3风格的差异,您将看到所有三行,包括来自“合并基础”( 8a2a26b...的父级)的a = 1

-X theirs extended-option 7告诉 Git,在发生冲突的情况下,它应该倾向于采用“他们的”更改。 因此,Git 不会尝试完全删除该行,而是保留父提交中的“更改分配”行。 请注意,在以后的樱桃挑选操作中,这将再次告诉 Git 更喜欢“他们的更改”,因此如果我们有一些其他需要解决的合并冲突,这可能会放弃我们之前在 rebase 中所做的任何事情。 允许 Git 这么大的权力有点危险:如果没有其他冲突,它工作正常,或者我们不必通过做 Git 认为“保持我们的”风格改变来解决任何问题,但任何时候你必须解决冲突,请始终检查结果,无论您是手动(通过自己解决冲突)还是自动(使用工具或扩展选项或其他)。


6由于这是 Python,因此将值1绑定名称a在技术上更正确。 当然, 技术上正确是最好的正确,因此有这个脚注。

7 Git 调用这些策略选项 但是-s设置策略,因此是策略选项 我们应该记住-s是策略选项,而-X是策略选项? 让我们将-X称为扩展选项。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM