[英]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
。 在下一次提交中,变量a
在a.py
的第一行中被赋值为1
。 接下来的四次提交将递增的值分配给a
。 最后创建第二个分支dummy2
指向最后一次提交。 dummy
和dummy2
的提交历史如下所示:
让我们假设在第二次提交( 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 show
或git 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
提交从1
到n
(包括此处)计数。
2此规则的唯一例外是所谓的根提交:每个非空存储库都有一个,因为根据定义,我们在新的空存储库中进行的第一次提交是第一次提交,因此没有父提交. 可以将多个根提交放入存储库,但这需要一些额外的工作。 不过,第一个是免费的,因为它是自然发生的。 没有理由需要额外的根提交:它们只是起点,这使它们成为历史的终结。 History ,在 Git 存储库中,只是提交。 因此,更早的历史是更早的提交,当你点击 / 根时,没有更早的东西。
3如何与一群数学或信息学人士开始争论:询问是否从零开始编号。
你问的是樱桃采摘——嗯,是关于变基的,但这意味着樱桃采摘——但首先从真正的合并开始是个好主意,因为在真正的合并中,这一切都更有意义。
当我们合并时,我们有两个分支。 这两个分支上的提交可能看起来有点像这样:
o--...--I--J <-- branch1 (HEAD)
/
...--B
\
o--...--K--L <-- branch2
较新的提交位于右侧,因此J
和L
是这两个分支上的最后提交。 我们做了一个git checkout branch1
或git switch branch1
来选择提交J
来处理/使用。
提交J
指向更早的提交I
,后者指向更多的提交,依此类推。 同时,提交L
指向一些更早的提交K
,它指向更多的提交。
现在我们运行git merge branch2
。 这告诉 Git:看看我们的提交J
和他们的提交L
。 通过历史追溯他们的父母联系。 Git 一直这样做,直到它到达提交B ,它位于两个分支上,因此充当合并基础提交。
这是合并过程的三个输入提交。 具有快照的提交B
是合并基础。 提交J
,它有一个快照,是我们的提交。 提交L
,它有一个快照,是他们的提交。 提交B
具有位于两个分支上的特殊属性,并且由于是两个分支上的最新提交,因此也比两个分支上的任何早期提交都“更好”。
git merge
的工作是合并工作。 为此,Git 必须找到更改。 它可以使用这些快照来做到这一点。
一种方法是将B
与顶行的子提交进行比较,然后将该提交与下一个提交进行比较,依此类推,直到我们到达I
和J
。 但是对于 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:
将来自B
、 J
和L
的所有三个输入文件保存在 Git 的索引中(又名staging area ,但这里的索引已被扩展,以便它可以容纳单个文件的三个副本)。 索引中的文件副本不是直接可见的,但是git status
会说这个文件是unmerged 。
写入您的工作树文件(您可以在编辑器中查看并打开以进行更改的文件)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
的更改。
但是——举一个非常简单的例子——假设在P
和C
之间,他们更改了文件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)
我们的新提交H
和C'
之间的区别应该与P
和C
之间的区别相同,除了行号等 - 以及任何需要解决的合并冲突。
在您的情况下,您有一个提交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.