为什么 git pull origin develop --rebase 会导致冲突而 git pull origin develop 不会?
Why does git pull origin develop --rebase cause conflict when git pull origin develop doesn't?
现在我通常使用
git pull origin develop
从开发分支获取最新更新。最近,我的团队一直在过渡到使用 rebase 而不是合并,所以我对某些东西有点困惑。在我的工作流程非常简单之前。我会先结帐到开发分支并使用
git checkout -b feature/foo
然后我会进行更改、提交然后推送它们。通常 develop 分支会因此进行一些更改,我会使用
git pull origin develop
获取最新的更改,只有当其他人修改同一个文件时才会发生冲突。但是,当我使用
git pull origin develop --rebase
我注意到我会与我自己的分支发生冲突,即使我是唯一修改它的人。这有什么特别的原因吗?有没有办法避免我与自己的分支之间的这些合并冲突?
首先,我们要注意git pull
主要由运行两个Git命令组成。这意味着它是一种方便的操作,让您键入 git pull
而不是 git fetch
输入 git .....
。第一个命令始终是 git fetch
,第二个是您的选择:默认为 git merge
,但您可以选择 git rebase
。当你想变基时,执行一个命令和执行两个命令几乎一样多,所以它毕竟不是很方便,我建议使用单独的 git fetch
和第二个命令,至少直到你'我们非常熟悉 Git.1
所以你的问题真的解决了一个更简单的问题:为什么 rebase 有时会发生合并没有的冲突? 对此有一个答案,实际上相当简单: Rebase主要就是重复cherry-picking,cherry-picking是合并的一种形式。因此,当您合并时,您有 一个 可能发生冲突的地方。如果你 rebase 十次提交,你有 ten 个地方可能会发生冲突。冲突本身也可能不同,但机会的规模是这里的主要因素。
1在带有子模块的存储库中,git pull
可以递归到子模块中,在这种情况下,它是两个以上的命令,其便利性方面变得很重要。你也可以默认配置git pull
到运行git rebase
,即使没有子模块也能重新出现便利。不过,我仍然鼓励新用户使用两个单独的命令——git pull
的语法有点奇怪,与几乎所有其他 Git 的东西有点不同,而且它很容易混淆。分配给 pull 的魔法太多了,而实际上 all 魔法来自第二个命令——你需要学习合并才能理解 rebase。
正在合并
尽管实现充满了棘手的小曲折,但合并背后的 idea 很简单。当我们要求 Git 合并时,我们有 "our work" 和 "their work"。 Git 需要弄清楚 我们 改变了什么,他们 改变了什么,并将这些改变结合起来。
为了做到这一点,Git 需要找到一个共同的起点。提交根本不是 一组更改 :它实际上是一个快照。 Git 可以将这些快照之一显示为其前身的不同之处,即提取两个快照并查看有什么不同。因此,如果我们从一些具有哈希 ID B
的提交开始,并且 他们 也从那个 same 提交开始:
C--D <-- our-branch (HEAD)
/
...--A--B
\
E--F <-- their-branch
然后 Git 可以将 B
中的快照与我们的最新快照 D
以及他们的最新快照 F
进行比较。 B
-vs-D
的不同之处在于 we 发生了变化。 B
-vs-F
的不同之处在于 他们 发生了变化。 Git 然后合并更改,将 合并的 更改应用到来自合并基础 B
的快照,并提交结果,将它连接到 两位前辈:
C--D
/ \
...--A--B G <-- our-branch (HEAD)
\ /
E--F <-- their-branch
要到达那里,Git 必须 运行:
git diff --find-renames <em>hash-of-B</em> <em>hash-of-D</em>
(我们改变了什么)
git diff --find-renames <em>hash-of-B</em> <em>hash-of-F</em>
(他们改变了什么)
当 Git 将这两个差异组合在一起时,我们和他们可能会在某些地方更改 相同的行 相同的文件。如果我们没有对这些行进行相同的 更改,Git 将声明冲突并在中间停止合并,而不是提交 G
yet,迫使我们收拾残局并完成合并以创建 G
.
摘樱桃
cherry-pick 背后的想法是复制 一个提交。要复制提交,我们可以 Git 将其转换为一组更改:
git diff --find-renames <em>hash-of-parent</em> <em>hash-of-commit</em>
然后我们可以将这些更改手动应用到其他地方,即其他提交。例如,如果我们有:
C--D <-- our-branch (HEAD)
/
...--A--B
\
E--F <-- their-branch
我们喜欢他们在 F
中所做的事情,但还不想 E
本身,我们可以区分 E
与 F
,看看他们做了什么做过。我们可以使用它来尝试对 D
中的快照进行相同的更改。然后我们给自己做一个新的提交——我们称它为 F'
表示 copy of F
:
C--D--F' <-- our-branch (HEAD)
/
...--A--B
\
E--F <-- their-branch
但是如果我们在 C
中进行了重大更改,或者他们在 E
中进行了重大更改,则可能很难从 E
-到-F
与我们在 D
中的快照对齐。为了 Git 帮助我们,并自动执行此复制 ,Git 想知道:E
之间有什么不同D
? 也就是说,Git 想要 运行:
git diff --find-renames <em>hash-of-E</em> <em>hash-of-D</em>
(我们在 C
和 E
中有什么)
git diff --find-renames <em>hash-of-E</em> <em>hash-of-F</em>
(他们在 F
中更改了什么)
但是等等,我们刚刚在 git merge
期间看到了上面相同的模式!事实上,这正是 Git 在这里所做的:它使用 与 git merge
相同的代码 ,它只是强制合并基础——这将是 B
用于常规合并 - 提交 E
,我们正在挑选的提交 F
的父级。 Git 现在将我们的更改与他们的更改结合起来,将组合的更改集应用到基础中的快照(在 E
中)并自行进行最终的 F'
提交,但是这次作为常规提交。
新提交也重新使用提交F
本身的提交消息,因此新提交F'
(它有一些新的散列ID,与 F
不同)很像 F
:git show
可能显示相同或非常相似的差异列表,当然还有相同的提交日志消息。
与 git merge
一样,这个合并过程——我喜欢将其称为 合并动词 ——可能会出错。如果确实出错,Git 会抱怨合并冲突,并在合并未完成时停止,并让您清理混乱并提交。当你提交时,Git 知道你正在完成一个 git cherry-pick
并在那个时候为你复制提交消息,使 F'
.
Rebase 重复 cherry-picking
做一个 git 变基 <em>target</em>
, Git:
- 列出您在您的分支上的提交,这些提交不是 可访问的 (一个技术术语:请参阅 Think Like (a) Git 来自 target ;
- 在适当的情况下修剪此列表——见下文;
- 检出提交 target 作为 "detached HEAD";
- 重复,一次一个提交,使用
git cherry-pick
复制列表中的每个提交。2
成功复制所有要复制的提交后,Git 将分支名称移动到复制列表的末尾。
假设我们从与之前类似的设置开始,但我将在此处列出更多提交:
C--D--E--F <-- our-branch (HEAD)
/
...--A--B
\
G--H <-- their-branch
我们运行 git rebase their-branch
,所以Git 列出要复制的提交:C-D-E-F
,按顺序。然后 Git 检出提交 H
作为 "detached HEAD":
C--D--E--F <-- our-branch
/
...--A--B
\
G--H <-- their-branch, HEAD
现在 Git 将挑选 C
进行复制。如果一切顺利:
C--D--E--F <-- our-branch
/
...--A--B
\
G--H <-- their-branch
\
C' <-- HEAD
Git 重复 D
、E
和 F
。完成后 D
和 E
我们处于这种状态:
C--D--E--F <-- our-branch
/
...--A--B
\
G--H <-- their-branch
\
C'-D'-E' <-- HEAD
在Git完成将F
复制到F'
之后,rebase的最后一步是将名称our-branch
拉到指向最终复制的提交,并且重新附加 HEAD
到它:
C--D--E--F [abandoned]
/
...--A--B
\
G--H <-- their-branch
\
C'-D'-E'-F' <-- our-branch (HEAD)
每个 cherry-pick 执行一个三向合并,操作的合并基础是被复制的提交的父级,"ours" 提交是分离的 HEAD
—请注意,最初是 他们的 提交 H
,随着我们的进步,它会随着时间的推移变成 "their commit H
plus our work"。 "theirs" 提交每次都是我们自己的提交。每个 cherry-pick 都可能有所有常见的合并冲突,但在大多数情况下,大多数没有。
有两个案例特别糟糕。其中之一,可能是最常见的,是当你自己的任何提交,例如在列表 C-D-E-F
中,本身是 G-H
链中的某些东西的精选(这通常是长于两次提交)——反之亦然,例如,也许 H
本质上是 D'
.
如果您或他们能够更早地轻松进行挑选,没有冲突,您的副本可能看起来几乎完全一样,甚至 100% 完全像 G-H
链中的一个。如果是这样,Git 可以识别它 是 这样的副本,并将其从 "to be copied" 列表中删除。在我们这里的例子中,如果 H
真的是 D'
,并且 Git 可以看到,Git 将从待复制列表中删除 D
,并且只复制 C-E-F
。但如果不是——例如,如果他们不得不改变他们的D
副本以制作H
——那么Git将尝试复制D
并且这些更改几乎肯定将与他们修改的H
.
冲突
如果您合并而不是复制,您将比较 B
与 H
(他们的)和 B
与 F
(您的)并且发生冲突的可能性是也许减少了。即使存在冲突,它们也可能更明显并且更容易解决。如果冲突是因为不必要的副本,根据我的经验,它们往往看起来更棘手。
另一个常见的问题案例是,在您的 C-D-E-F
链中,您最后几次提交是您专门为使合并更容易而做的事情。也就是说,有人可能会这样说:我们更改了 foo 子系统,现在您需要第三个参数 并且您在挑选更改后在 F
中添加了第三个参数在 E
。复制 C
和 D
时会发生冲突。您可能会跳过复制 E
,因为它 是 一个精选,然后在您解决了 D
中的冲突后,就不需要复制 F
和 E
,但这是需要修复的两份副本,一份自动删除,一份需要您自己手动删除。
所以,最后,git merge
进行了一次合并,但是 git rebase
进行了多次挑选,每一次都是——在内部——一次合并,每一次都可以导致合并冲突。变基得到更多冲突也就不足为奇了!
2从技术上讲,普通(非交互式)git rebase
通常 不会 使用 git cherry-pick
.相反,它实际上使用 git format-patch ... | git am ...
。使用 git rebase -i
总是使用 git cherry-pick
,而 git rebase -m
强制非交互式 git rebase
使用 git cherry-pick
。简单的 rebase 避免它的事实主要只是古代(可能是 2008 年左右之前)Git,在 cherry-pick 被教导进行适当的三向合并之前。
git am
步骤使用-3
,因此如果补丁失败,Git将"fall back"进行三向合并。结果通常是相同的,但是 format-patch-pipe-to-am 方法永远找不到重命名的文件。这使得格式补丁样式更快,但不是那么好。
现在我通常使用
git pull origin develop
从开发分支获取最新更新。最近,我的团队一直在过渡到使用 rebase 而不是合并,所以我对某些东西有点困惑。在我的工作流程非常简单之前。我会先结帐到开发分支并使用
git checkout -b feature/foo
然后我会进行更改、提交然后推送它们。通常 develop 分支会因此进行一些更改,我会使用
git pull origin develop
获取最新的更改,只有当其他人修改同一个文件时才会发生冲突。但是,当我使用
git pull origin develop --rebase
我注意到我会与我自己的分支发生冲突,即使我是唯一修改它的人。这有什么特别的原因吗?有没有办法避免我与自己的分支之间的这些合并冲突?
首先,我们要注意git pull
主要由运行两个Git命令组成。这意味着它是一种方便的操作,让您键入 git pull
而不是 git fetch
输入 git .....
。第一个命令始终是 git fetch
,第二个是您的选择:默认为 git merge
,但您可以选择 git rebase
。当你想变基时,执行一个命令和执行两个命令几乎一样多,所以它毕竟不是很方便,我建议使用单独的 git fetch
和第二个命令,至少直到你'我们非常熟悉 Git.1
所以你的问题真的解决了一个更简单的问题:为什么 rebase 有时会发生合并没有的冲突? 对此有一个答案,实际上相当简单: Rebase主要就是重复cherry-picking,cherry-picking是合并的一种形式。因此,当您合并时,您有 一个 可能发生冲突的地方。如果你 rebase 十次提交,你有 ten 个地方可能会发生冲突。冲突本身也可能不同,但机会的规模是这里的主要因素。
1在带有子模块的存储库中,git pull
可以递归到子模块中,在这种情况下,它是两个以上的命令,其便利性方面变得很重要。你也可以默认配置git pull
到运行git rebase
,即使没有子模块也能重新出现便利。不过,我仍然鼓励新用户使用两个单独的命令——git pull
的语法有点奇怪,与几乎所有其他 Git 的东西有点不同,而且它很容易混淆。分配给 pull 的魔法太多了,而实际上 all 魔法来自第二个命令——你需要学习合并才能理解 rebase。
正在合并
尽管实现充满了棘手的小曲折,但合并背后的 idea 很简单。当我们要求 Git 合并时,我们有 "our work" 和 "their work"。 Git 需要弄清楚 我们 改变了什么,他们 改变了什么,并将这些改变结合起来。
为了做到这一点,Git 需要找到一个共同的起点。提交根本不是 一组更改 :它实际上是一个快照。 Git 可以将这些快照之一显示为其前身的不同之处,即提取两个快照并查看有什么不同。因此,如果我们从一些具有哈希 ID B
的提交开始,并且 他们 也从那个 same 提交开始:
C--D <-- our-branch (HEAD)
/
...--A--B
\
E--F <-- their-branch
然后 Git 可以将 B
中的快照与我们的最新快照 D
以及他们的最新快照 F
进行比较。 B
-vs-D
的不同之处在于 we 发生了变化。 B
-vs-F
的不同之处在于 他们 发生了变化。 Git 然后合并更改,将 合并的 更改应用到来自合并基础 B
的快照,并提交结果,将它连接到 两位前辈:
C--D
/ \
...--A--B G <-- our-branch (HEAD)
\ /
E--F <-- their-branch
要到达那里,Git 必须 运行:
git diff --find-renames <em>hash-of-B</em> <em>hash-of-D</em>
(我们改变了什么)git diff --find-renames <em>hash-of-B</em> <em>hash-of-F</em>
(他们改变了什么)
当 Git 将这两个差异组合在一起时,我们和他们可能会在某些地方更改 相同的行 相同的文件。如果我们没有对这些行进行相同的 更改,Git 将声明冲突并在中间停止合并,而不是提交 G
yet,迫使我们收拾残局并完成合并以创建 G
.
摘樱桃
cherry-pick 背后的想法是复制 一个提交。要复制提交,我们可以 Git 将其转换为一组更改:
git diff --find-renames <em>hash-of-parent</em> <em>hash-of-commit</em>
然后我们可以将这些更改手动应用到其他地方,即其他提交。例如,如果我们有:
C--D <-- our-branch (HEAD)
/
...--A--B
\
E--F <-- their-branch
我们喜欢他们在 F
中所做的事情,但还不想 E
本身,我们可以区分 E
与 F
,看看他们做了什么做过。我们可以使用它来尝试对 D
中的快照进行相同的更改。然后我们给自己做一个新的提交——我们称它为 F'
表示 copy of F
:
C--D--F' <-- our-branch (HEAD)
/
...--A--B
\
E--F <-- their-branch
但是如果我们在 C
中进行了重大更改,或者他们在 E
中进行了重大更改,则可能很难从 E
-到-F
与我们在 D
中的快照对齐。为了 Git 帮助我们,并自动执行此复制 ,Git 想知道:E
之间有什么不同D
? 也就是说,Git 想要 运行:
git diff --find-renames <em>hash-of-E</em> <em>hash-of-D</em>
(我们在C
和E
中有什么)git diff --find-renames <em>hash-of-E</em> <em>hash-of-F</em>
(他们在F
中更改了什么)
但是等等,我们刚刚在 git merge
期间看到了上面相同的模式!事实上,这正是 Git 在这里所做的:它使用 与 git merge
相同的代码 ,它只是强制合并基础——这将是 B
用于常规合并 - 提交 E
,我们正在挑选的提交 F
的父级。 Git 现在将我们的更改与他们的更改结合起来,将组合的更改集应用到基础中的快照(在 E
中)并自行进行最终的 F'
提交,但是这次作为常规提交。
新提交也重新使用提交F
本身的提交消息,因此新提交F'
(它有一些新的散列ID,与 F
不同)很像 F
:git show
可能显示相同或非常相似的差异列表,当然还有相同的提交日志消息。
与 git merge
一样,这个合并过程——我喜欢将其称为 合并动词 ——可能会出错。如果确实出错,Git 会抱怨合并冲突,并在合并未完成时停止,并让您清理混乱并提交。当你提交时,Git 知道你正在完成一个 git cherry-pick
并在那个时候为你复制提交消息,使 F'
.
Rebase 重复 cherry-picking
做一个 git 变基 <em>target</em>
, Git:
- 列出您在您的分支上的提交,这些提交不是 可访问的 (一个技术术语:请参阅 Think Like (a) Git 来自 target ;
- 在适当的情况下修剪此列表——见下文;
- 检出提交 target 作为 "detached HEAD";
- 重复,一次一个提交,使用
git cherry-pick
复制列表中的每个提交。2
成功复制所有要复制的提交后,Git 将分支名称移动到复制列表的末尾。
假设我们从与之前类似的设置开始,但我将在此处列出更多提交:
C--D--E--F <-- our-branch (HEAD)
/
...--A--B
\
G--H <-- their-branch
我们运行 git rebase their-branch
,所以Git 列出要复制的提交:C-D-E-F
,按顺序。然后 Git 检出提交 H
作为 "detached HEAD":
C--D--E--F <-- our-branch
/
...--A--B
\
G--H <-- their-branch, HEAD
现在 Git 将挑选 C
进行复制。如果一切顺利:
C--D--E--F <-- our-branch
/
...--A--B
\
G--H <-- their-branch
\
C' <-- HEAD
Git 重复 D
、E
和 F
。完成后 D
和 E
我们处于这种状态:
C--D--E--F <-- our-branch
/
...--A--B
\
G--H <-- their-branch
\
C'-D'-E' <-- HEAD
在Git完成将F
复制到F'
之后,rebase的最后一步是将名称our-branch
拉到指向最终复制的提交,并且重新附加 HEAD
到它:
C--D--E--F [abandoned]
/
...--A--B
\
G--H <-- their-branch
\
C'-D'-E'-F' <-- our-branch (HEAD)
每个 cherry-pick 执行一个三向合并,操作的合并基础是被复制的提交的父级,"ours" 提交是分离的 HEAD
—请注意,最初是 他们的 提交 H
,随着我们的进步,它会随着时间的推移变成 "their commit H
plus our work"。 "theirs" 提交每次都是我们自己的提交。每个 cherry-pick 都可能有所有常见的合并冲突,但在大多数情况下,大多数没有。
有两个案例特别糟糕。其中之一,可能是最常见的,是当你自己的任何提交,例如在列表 C-D-E-F
中,本身是 G-H
链中的某些东西的精选(这通常是长于两次提交)——反之亦然,例如,也许 H
本质上是 D'
.
如果您或他们能够更早地轻松进行挑选,没有冲突,您的副本可能看起来几乎完全一样,甚至 100% 完全像 G-H
链中的一个。如果是这样,Git 可以识别它 是 这样的副本,并将其从 "to be copied" 列表中删除。在我们这里的例子中,如果 H
真的是 D'
,并且 Git 可以看到,Git 将从待复制列表中删除 D
,并且只复制 C-E-F
。但如果不是——例如,如果他们不得不改变他们的D
副本以制作H
——那么Git将尝试复制D
并且这些更改几乎肯定将与他们修改的H
.
如果您合并而不是复制,您将比较 B
与 H
(他们的)和 B
与 F
(您的)并且发生冲突的可能性是也许减少了。即使存在冲突,它们也可能更明显并且更容易解决。如果冲突是因为不必要的副本,根据我的经验,它们往往看起来更棘手。
另一个常见的问题案例是,在您的 C-D-E-F
链中,您最后几次提交是您专门为使合并更容易而做的事情。也就是说,有人可能会这样说:我们更改了 foo 子系统,现在您需要第三个参数 并且您在挑选更改后在 F
中添加了第三个参数在 E
。复制 C
和 D
时会发生冲突。您可能会跳过复制 E
,因为它 是 一个精选,然后在您解决了 D
中的冲突后,就不需要复制 F
和 E
,但这是需要修复的两份副本,一份自动删除,一份需要您自己手动删除。
所以,最后,git merge
进行了一次合并,但是 git rebase
进行了多次挑选,每一次都是——在内部——一次合并,每一次都可以导致合并冲突。变基得到更多冲突也就不足为奇了!
2从技术上讲,普通(非交互式)git rebase
通常 不会 使用 git cherry-pick
.相反,它实际上使用 git format-patch ... | git am ...
。使用 git rebase -i
总是使用 git cherry-pick
,而 git rebase -m
强制非交互式 git rebase
使用 git cherry-pick
。简单的 rebase 避免它的事实主要只是古代(可能是 2008 年左右之前)Git,在 cherry-pick 被教导进行适当的三向合并之前。
git am
步骤使用-3
,因此如果补丁失败,Git将"fall back"进行三向合并。结果通常是相同的,但是 format-patch-pipe-to-am 方法永远找不到重命名的文件。这使得格式补丁样式更快,但不是那么好。