在rebase期间,我将我的本地功能分支同步到上游分支以完成拉取请求,我尝试使用所有三种方法(git rebase,git rebase -i和git merge),并且每个方法都提供了完全不同的经验,当谈到冲突解决。
Git merge立刻向我展示了我所有的冲突。我解决了这些问题并在完成所有修改后添加了更改。正如所料,合并搞砸了我的历史,我不得不再次回归。
Git Rebase通过两个步骤引导我完成冲突。在每个我添加我的更改并继续rebase后。在我之间我失去了一个补丁,不得不重新开始。
互动重建就像魅力一样。它引导我完成提交的冲突提交,并在每个解决方案之后,它再次从功能分支的基础再次快速转发到下一个冲突。我可以确保正确地包含提交共同作者,并且最终甚至不需要添加“合并”或“rebase”提交,在完成后坐在分支的头部。
我对何时使用它们有一个概念性的理解,但是为什么rebase和交互式rebase的行为如此完全不同,即使没有交互式编辑修订版?为什么即使使用git merge和git rebase,当它们似乎做得很糟糕并且更容易弄乱历史中的某些东西?
答案 0 :(得分:2)
...为什么rebase和交互式rebase表现得如此截然不同
作为一般规则,他们不应该。他们有时会做,并解释为什么是棘手的。一个快速的底线外卖是非交互式git rebase
使用 - 井,有时使用 - git format-patch
并将其输出管道传输到git am
,这个可以,但通常不会与使用git cherry-pick
的交互式rebase执行相同的操作。
从历史上看,这是git rebase
的唯一形式,因为它 的行为有点不同 - 并且可以更好地工作 - Git作者选择不将每个人都转换为"总是挑选"方法
为什么即使使用git merge和git rebase,当它们似乎做得很糟糕并且更容易弄乱历史中的某些东西?
首先,git merge
和git 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 merge
和L
:
R
此次合并基础不是以前的提交...--o--o--o--o---M--L <-- master (HEAD)
\ /
o--o--o--o--R <-- develop
,而是以前的提交*
!因此,合并提交R
的存在会改变 next M
命令的 next 合并基础。
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为提交develop
和git patch-id
计算git patch-id
。如果这些提交与提交F
,G
或git 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 apply
(git 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-patch
和HEAD
重复此过程,给出:
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
虽然如果还有其他方法可以找到旧提交(现有标记或分支名称通向它们),那么旧提交根本就不会被放弃,你会看到两者新旧。
旧式develop
和interactive 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
,因为C
是C
的父级。 那个是有道理的。这个至少在开始时并不是这样。 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的类型。
最后一点是基于-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
内部提供支持。)