git rebase,git rebase -i和git merge之间的低级别差异

时间:2017-11-15 13:26:04

标签: git merge rebase git-interactive-rebase

在rebase期间,我将我的本地功能分支同步到上游分支以完成拉取请求,我尝试使用所有三种方法(git rebase,git rebase -i和git merge),并且每个方法都提供了完全不同的经验,当谈到冲突解决。

Git merge立刻向我展示了我所有的冲突。我解决了这些问题并在完成所有修改后添加了更改。正如所料,合并搞砸了我的历史,我不得不再次回归。

Git Rebase通过两个步骤引导我完成冲突。在每个我添加我的更改并继续rebase后。在我之间我失去了一个补丁,不得不重新开始。

互动重建就像魅力一样。它引导我完成提交的冲突提交,并在每个解决方案之后,它再次从功能分支的基础再次快速转发到下一个冲突。我可以确保正确地包含提交共同作者,并且最终甚至不需要添加“合并”或“rebase”提交,在完成后坐在分支的头部。

我对何时使用它们有一个概念性的理解,但是为什么rebase和交互式rebase的行为如此完全不同,即使没有交互式编辑修订版?为什么即使使用git merge和git rebase,当它们似乎做得很糟糕并且更容易弄乱历史中的某些东西?

1 个答案:

答案 0 :(得分:2)

  

...为什么rebase和交互式rebase表现得如此截然不同

作为一般规则,他们不应该。他们有时会做,并解释为什么是棘手的。一个快速的底线外卖是非交互式git rebase使用 - 井,有时使用 - git format-patch并将其输出管道传输到git am,这个可以,但通常不会与使用git cherry-pick的交互式rebase执行相同的操作。

从历史上看,这是git rebase唯一形式,因为它 的行为有点不同 - 并且可以更好地工作 - Git作者选择不将每个人都转换为"总是挑选"方法

冗长且涉及的答案

  

为什么即使使用git merge和git rebase,当它们似乎做得很糟糕并且更容易弄乱历史中的某些东西?

首先,git mergegit rebase有不同的目标,因此它们并非完全可比。您已经意识到Git完全是关于提交的,分支名称只是一种查找提交 - 一个特定提交的方式,Git从中找到所有以前的提交 - 但是让'这里做了一些术语来帮助我们谈论它:

...--o--*--o--L   <-- master (HEAD)
         \
          o--o--R   <-- develop

请注意,我们可以将其重新绘制为:

          o--L   <-- master (HEAD)
         /
...--o--*
         \
          o--o--R   <-- develop

要强调的是,从提交*开始,所有这些提交同时在两个分支上。名称master(也是当前分支HEAD)标识提交L(对于&#34;左&#34;或&#34;本地&#34;)。名称develop标识提交R(&#34;右&#34;或&#34;远程&#34;)。它是那两个标识其父提交的提交,如果我们或Git小心地向后跟踪每个父提交,则两个提交流最终将永久重新加入,在这种情况下 - 在提交*。 / p>

关于git merge的说明,我们需要谈谈变基

运行git merge要求Git找到合并基础,即提交*,然后将该合并基础与两个分支提示提交L(本地或{{}中的每一个进行比较1}})和--ours(远程或R)。无论左/本地方面有何不同,我们必须改变。无论在右侧/远侧都有什么不同,它们必须已经改变。合并机制,执行合并行为(&#34;合并&#34;作为动词),结合了这两组变化。

--theirs命令(假设它像这样进行真正的合并,即你不进行快进或压缩)以这种方式使用合并机制来计算那些文件集应该提交,然后进行新的合并提交。这种提交 - 使用单词&#34; merge&#34;作为形容词,或简称为&#34;合并&#34;,使用&#34; merge&#34;作为名词 - 有两个父母:git merge是第一个父母,L是第二个父母。 文件由merge-as-a-verb动作决定;提交本身合并。如果我们把它画成:

R

我们稍后可以添加更多提交,此时我们可以再次运行...--o--o--o--L---M <-- master (HEAD) \ / o--o--R <-- develop ,选择新的git mergeL

R

此次合并基础不是以前的提交...--o--o--o--o---M--L <-- master (HEAD) \ / o--o--o--o--R <-- develop ,而是以前的提交*!因此,合并提交R的存在会改变 next M命令的 next 合并基础。

任何rebase的基础知识

git merge的作用非常不同:它标识了 copy 的一些提交,然后复制它们。

要复制的提交集是可以从当前分支(即git rebase)访问的提交,这些提交不能从{你提供的{1}}参数:

HEAD

此时,在内部,Git会生成一个提交哈希列表。如果提交图仍然如下所示:

<upstream>

并且$ git checkout develop $ git rebase <upstream-hash> # or, easier, git rebase master 的参数标识提交...--o--*--F--G <-- master \ C--D--E <-- develop (HEAD) git rebase之后的任何提交 - 当然包括*提示 master,这通常是我们在这里选择的 - 然后要复制的提交哈希集是master的那些。

此集合中的某些提交可能会故意丢弃。这包括:

  • 任何合并提交,因为它们无法复制(但这里没有 - 大多数情况下这会消除G中任何合并回C--D--E);
  • master匹配上游提交的任何提交。

后者意味着Git为提交developgit patch-id计算git patch-id。如果这些提交与提交FGgit patch-id的{​​{1}}匹配,那么这些提交会从&#34;中复制&#34;列表。

(如果使用C模式,Git可能会从列表中删除其他提交。很难描述这一点。请参阅Git rebase - commit select in fork-point mode。)

Git现在开始复制过程。这是非交互式和交互式rebase可能不同的地方。两者都以&#34;分离HEAD&#34;开始,将其设置为复制目标。默认为D提交,在我们的示例中为commit E

正常的非交互式方法

通常a non-interactive git rebase runs git format-patch on the selected commits,然后是feeds the output to git am

--fork-point

<upstream>重复调用G。每个git format-patch -k --stdout --full-index --cherry-pick --right-only \ --src-prefix=a/ --dst-prefix=b/ --no-renames --no-cover-letter \ $git_format_patch_opt \ "$revisions" ${restrict_revision+^$restrict_revision} \ >"$GIT_DIR/rebased-patches" ... git am $git_am_opt --rebasing --resolvemsg="$resolvemsg" \ $allow_rerere_autoupdate \ ${gpg_sign_opt:+"$gpg_sign_opt"} <"$GIT_DIR/rebased-patches" 尝试直接应用差异:查找上下文,验证上下文未更改,然后添加和删除git am流中嵌入的git apply -3输出中显示的行。

如果验证步骤失败,git applygit diff很重要)会使用回退方法:格式修补程序输出中的git format-patch行标识合并基础< / em>每个文件的版本,因此git apply -3可以提取该合并基础版本,直接将补丁应用于它 - 这应该始终有效 - 并将其用作&#34;版本R&#34;。合并基础版本当然是合并基础版本,文件的当前或-3版本充当&#34;版本L&#34;。我们现在拥有了执行该特定文件的常规index所需的一切。 我们此时只合并一个文件,,这只是&#34;合并为动词&#34;。 (另请参阅下面git apply的说明。)

这种三方合并可以一如既往地成功或失败。无论发生哪种情况,Git都可以继续使用此特定补丁中的其余文件。如果直接应用所有修补程序,或者由于三向合并回退 - Git将使用保存在HEAD流中的消息文本从结果进行提交。这会将原始提交复制到一个新的,但至少略有不同的提交,其父级是 git merge的提交:

git cherry-pick

对提交git format-patchHEAD重复此过程,给出:

                C'   <-- HEAD
               /
...--o--*--F--G   <-- master
         \
          C--D--E   <-- develop

完成后,D&#34;剥离标签&#34; E关闭旧的提交链并将其粘贴在新的提交链上。理想情况下,旧的提交被放弃,只能通过reflogs找到,暂时可以找到特殊名称 C'-D'-E' <-- HEAD / ...--o--*--F--G <-- master \ C--D--E <-- develop

git rebase

虽然如果还有其他方法可以找到旧提交(现有标记或分支名称通向它们),那么旧提交根本就不会被放弃,你会看到两者新旧。

交互式rebase

旧式developinteractive git-rebase--interactive.sh is that the latter writes a big instructions file including help text之间存在明显差异,并允许您对其进行编辑。但即使你只是按原样写出来,actual code to implement each pick command runs git cherry-pick。 (此代码已在最新版本的Git中进行了修订,现在用C语言实现,而不是shell脚本,但shell脚本更清晰,两者应该表现相同,所以我已经链接到了脚本此处。)

ORIG_HEAD运行时,总是进行三向合并(至少在任何一个半现代的Git中:可能有一个使用{{1}的旧版本在某些时候;我对早期的行为有不同的模糊记忆。这种三向合并的不寻常之处在于合并基础是被挑选的提交的父级。这意味着如果我们要复制提交 C'-D'-E' <-- develop (HEAD) / ...--o--*--F--G <-- master \ C--D--E [abandoned] ,就像在这种状态下一样:

git-rebase--am.sh

此特定merge-as-a-verb操作的合并基础不是提交git cherry-pick。它根本不在git format-patch | git am -3上提交:它提交D

我们将 C' <-- HEAD / ...--o--*--F--G <-- master \ C--D--E <-- develop 复制到*时的合并基础为master,因为CC的父级。 那个是有道理的。这个至少在开始时并不是这样。 C'如何成为合并基础?但它是:Git运行*以查看&#34;我们更改了什么&#34;,并将其与*结合起来(&#34;他们改变了什么&#34;)。

如果这些更改中的任何一个重叠,我们就会发生合并冲突。如果没有,Git将保持&#34;我们改变了什么&#34;并简单地添加它&#34;他们改变了什么&#34;。请注意,这两个比较(这两个C操作)运行 commit-wide ,而不仅仅是在一个特定文件上。这允许cherry-pick查找在两个分支之一中重命名的文件。然后Git在每个文件上执行merge-as-a-verb。完成后,如果没有冲突,Git会从结果文件中进行普通(非合并)提交。

假设一切顺利,C被复制到git diff --find-renames C C',Git继续挑选git diff --find-renames C D。这次git diff --find-rename是合并基础。该操作与以前一样工作:我们找到重命名,将所有文件合并为动词,并进行D的普通非合并提交。

最后,与非交互式rebase一样,Git将旧分支提交中的分支名称剥离并将其放在新提示上。

非互动与互动的更多特点

使用D'的非交互式rebase会产生许多副作用。最重要的是E字面上无法产生&#34;空&#34; patch-a commit不对源进行任何更改 - 因此,如果您使用D来保持&#34;这样的提交,非交互式rebase使用E'

第二个是因为git format-patch被告知git format-patch(参见上面的实际命令),它表示文件重命名为&#34;删除旧文件,添加新文件&#34;。这可以防止Git发现一些冲突。 (只要待删除的文件在补丁中,它至少可以检测到删除/修改冲突,但它无法检测到删除/重命名冲突,并且在补丁中超出&#34; 34;重命名,它什么都没有注意到。)当然,如果我们可以构建一个由于显然 - 有效的上下文而应用补丁的情况,即使是三 - 方式合并可能会发现匹配的上下文来自代码的移动的副本,我们可以成功应用补丁,其中三向合并将检测冲突,或将其应用于其他地方。

(我打算在某个时候构建一个例子,但从来没有时间去做。)

如果您使用-k选项,指定rebase应使用合并机制,或git cherry-pick选项或git format-patch(两者都暗示使用合并机制),这也是迫使Git使用樱桃挑选。然而,这实际上是第三种变种!

The rebase type-selection happens in git-rebase.sh, well into the script

--no-renames

请注意隐藏状态文件的位置,跟踪您是否正在停止让您编辑(交互式rebase)或由于冲突(正在进行中)的正在进行的-m rebase),取决于rebase的类型。

Git笔记

最后一点是基于-s <strategy>的rebase不会运行-X <extended-option>。另外两个呢。这意味着您在使用if test -n "$interactive_rebase" then type=interactive state_dir="$merge_dir" elif test -n "$do_merge" then type=merge state_dir="$merge_dir" else type=am state_dir="$apply_dir" fi 时会删除对原始提交所做的注释,但在使用交互式rebase或git rebase时会保留这些注释。

(这对我来说似乎是一个错误,但也许是故意的。保留笔记会有点棘手,因为我们需要从旧提交哈希到新提交哈希的映射。这需要在am内部提供支持。)