The*_*tan 1 git merge rebase git-interactive-rebase
在 rebase 期间,我将本地功能分支同步到上游分支以完成拉取请求,我尝试使用所有三种方法(git rebase、git rebase -i 和 git merge),每种方法都提供了完全不同的体验,当它涉及解决冲突。
Git merge 一次向我展示了我所有的冲突。我解决了它们并在解决所有这些问题后添加了更改。正如预期的那样,合并弄乱了我的历史,我不得不再次恢复。
Git Rebase 分两步引导我解决冲突。在每个中,我添加了我的更改并在此之后继续 rebase。在这期间,我丢失了一个补丁,不得不重新开始。
交互式变基工作就像一个魅力。它引导我逐个提交冲突,在每次解决之后,它再次开始从功能分支的基础快速转发到下一个冲突。我可以确保提交的共同作者被正确包含,最后甚至不需要添加“合并”或“变基”提交,完成后坐在分支的头部。
我对何时使用它们中的每一个都有概念性的理解,但是为什么即使没有交互式编辑修订版,rebase 和交互式 rebase 的行为也有如此大的不同?为什么 git merge 和 git rebase 甚至使用,当它们似乎做的事情很糟糕并且更容易弄乱历史中的某些东西时?
... 为什么 rebase 和交互式 rebase 的行为完全不同
作为一般规则,他们不应该。他们有时会这样做,而要准确解释原因很棘手。一个快速的底线外卖的是,非交互式git rebase的使用井,有时使用说明-git format-patch和管道的输出git am,这可以,但通常不会,做同样的事情互动变基,它使用git cherry-pick来代替。
从历史上看,这是唯一的形式git rebase,并且因为它确实表现得有点不同,并能更好地工作,Git作者选择不切换大家一个“永远摘樱桃”的方法。
为什么 git merge 和 git rebase 甚至使用,当它们似乎做的事情很糟糕并且更容易弄乱历史中的某些东西时?
首先,git merge并git rebase有不同的目标,所以他们不是所有的可比性。你已经知道 Git 是关于提交的,分支名称只是一种查找提交的方式——一个特定的提交,Git 从中找到所有以前的提交——但让我们在这里做一些术语来帮助我们讨论它:
...--o--*--o--L <-- master (HEAD)
\
o--o--R <-- develop
Run Code Online (Sandbox Code Playgroud)
请注意,我们可以将其重新绘制为:
o--L <-- master (HEAD)
/
...--o--*
\
o--o--R <-- develop
Run Code Online (Sandbox Code Playgroud)
强调一下,从*向后提交,所有这些提交都同时在两个分支上。namemaster也是当前分支HEAD,标识提交L(用于“左”或“本地”)。该名称develop标识提交R(“正确”或“远程”)。正是这两个提交标识了它们的父提交,如果我们——或 Git——仔细地向后跟踪每个父提交,这两个提交流最终会重新加入——在这种情况下——在 commit 处永久地*。
git merge,我们需要谈论 rebase运行git merge要求 Git 找到合并基础,即 commit *,然后将该合并基础与两个分支提示提交L(本地或--ours)和R(远程或--theirs)中的每一个进行比较。无论左侧/本地有什么不同,我们都必须改变。无论右侧/远端有什么不同,它们都必须改变。执行合并行为(“合并”作为动词)的合并机制将这两组更改组合在一起。
该git merge命令(假设它进行了这样的真正合并,即您没有进行快进或压缩)以这种方式使用合并机制来计算应该提交的文件集,然后进行新的合并提交. 这种提交——使用“合并”这个词作为形容词,或者简称为“合并”,使用“合并”作为名词——有两个父母:L第一个父母,R第二个。该文件被合并作为一种动词动作确定; 提交本身就是一个合并。如果我们把它画成:
...--o--o--o--L---M <-- master (HEAD)
\ /
o--o--R <-- develop
Run Code Online (Sandbox Code Playgroud)
然后我们可以稍后添加更多提交,此时我们可以git merge再次运行,选择一个新的L和R:
...--o--o--o--o---M--L <-- master (HEAD)
\ /
o--o--o--o--R <-- develop
Run Code Online (Sandbox Code Playgroud)
这次的合并基础不是以前的提交*,而是以前的提交R!因此合并提交的存在M改变了下一个 git merge命令的下一个合并基础。
什么git rebase是非常不同的:它识别一些提交到copy,然后复制它们。
要复制的提交集是可从当前分支(即HEAD)访问的提交,但无法从<upstream>您提供的参数访问:
$ git checkout develop
$ git rebase <upstream-hash> # or, easier, git rebase master
Run Code Online (Sandbox Code Playgroud)
此时,Git 在内部生成一个提交哈希列表。如果提交图仍然如下所示:
...--o--*--F--G <-- master
\
C--D--E <-- develop (HEAD)
Run Code Online (Sandbox Code Playgroud)
以及git rebase标识提交*或之后的任何提交的参数master——当然,包括masterG的提示,这通常是我们在这里选择的——那么要复制的提交哈希集是那些用于C--D--E.
这个集合中的一些提交可能会被故意丢弃。这包括:
masterback into 的任何合并develop);git patch-id与上游提交匹配的提交。后者意味着 Git 计算git patch-idfor 提交F和G. 如果那些与git patch-idcommits C、D或匹配E,则这些提交将从“复制”列表中删除。
(如果使用--fork-pointmode,Git 可能会从列表中抛出额外的提交。描述这个很难。请参阅Git rebase - commit select in fork-point mode。)
Git 现在开始复制过程。这就是非交互式和交互式 rebase 可能不同的地方。两者都从“分离 HEAD”开始,将其设置为复制的目标。这默认为<upstream>提交,在我们的例子中是 commit G。
通常,非交互式在选定的 commits 上git rebase运行git format-patch,然后将输出提供给git am:
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"
Run Code Online (Sandbox Code Playgroud)
这会git am反复调用git apply -3. 每个都git apply尝试直接应用差异:找到上下文,验证上下文未更改,然后添加和删除git diff嵌入在git format-patch流中的输出中显示的行。
如果验证步骤失败,git apply -3(这-3很重要)使用回退方法:index格式补丁输出中的行标识每个文件的合并基础版本,因此git apply可以提取该合并基础版本,直接对其应用补丁——这应该始终有效——并将其用作“R 版”。合并基础版本当然是合并基础版本,HEAD文件的当前或版本充当“版本L”。我们现在拥有git merge了对该特定文件进行常规操作所需的一切。 此时我们只合并一个文件,这只是“作为动词合并”。(另请参阅下面的描述git cherry-pick。)
这种三向合并可以像往常一样成功或失败。无论发生什么情况,Git 都可以继续处理此特定补丁中的其余文件。如果所有补丁都适用——无论是直接应用,还是作为三向合并回退的结果——Git 将使用保存在git format-patch流中的消息文本根据结果进行提交。这将原始提交复制到一个新的,但至少略有不同的提交,其父提交是 HEAD:
C' <-- HEAD
/
...--o--*--F--G <-- master
\
C--D--E <-- develop
Run Code Online (Sandbox Code Playgroud)
这个过程对提交D和重复重复E,给出:
C'-D'-E' <-- HEAD
/
...--o--*--F--G <-- master
\
C--D--E <-- develop
Run Code Online (Sandbox Code Playgroud)
完成后,从旧的提交链上git rebase“剥离标签”develop并将其贴在新的提交链上。理想情况下,旧的提交被放弃,只能通过 reflogs 和临时的特殊名称找到ORIG_HEAD:
C'-D'-E' <-- develop (HEAD)
/
...--o--*--F--G <-- master
\
C--D--E [abandoned]
Run Code Online (Sandbox Code Playgroud)
虽然如果有其他方法可以找到旧提交(导致它们的现有标签或分支名称),旧提交毕竟不会被放弃,您将看到旧提交和新提交。
旧式git-rebase--am.sh和交互式git-rebase--interactive.sh之间的明显区别在于,后者编写了一个包含帮助文本的大说明文件,并允许您对其进行编辑。但即使你只是按原样写出来,实现每个pick命令git cherry-pick的实际代码也会运行。(这段代码在最新版本的Git中已经修改,现在用C实现,而不是shell脚本,但是shell脚本要清晰得多,而且两者的行为应该是一样的,所以我链接到了脚本这里。)
当git cherry-pick运行时,它总是做一个三方合并(至少在任何甚至半现代的Git:有可能是一个老所用git format-patch | git am -3,在某些时候,我有不同的行为在成立初期的模糊记忆)。这种三向合并的不同寻常之处在于合并基础是被挑选出来的提交的父级。这意味着如果我们要复制 commit D,就像在这种状态下一样:
C' <-- HEAD
/
...--o--*--F--G <-- master
\
C--D--E <-- develop
Run Code Online (Sandbox Code Playgroud)
此特定合并作为动词操作的合并基础不是 commit *。它甚至根本就不是一个提交master:它是 commit C。
我们复制C到时的合并基础C'是*,因为*是C的父级。 那个有道理。这个没有,至少一开始没有。怎么可能C是合并基地?但它是:Git 运行git diff --find-renames C C'是为了查看“我们改变了什么”,并将其与git diff --find-renames C D(“他们改变了什么”)结合起来。
如果这些更改中的任何一个重叠,我们就会遇到合并冲突。如果没有,Git 将保留“我们更改的内容”并简单地添加“他们更改的内容”。请注意,这两个比较,这两个git diff --find-rename操作,在提交范围内运行,而不仅仅是在一个特定文件上运行。这允许cherry-pick 找到在两个分支之一中重命名的文件。Git 然后对每个文件执行合并作为动词。完成后,如果没有冲突,Git 会从结果文件中进行普通(非合并)提交。
假设一切顺利,D并被复制到D',Git 继续进行cherry-pick E。这次D是合并基地。该操作和以前一样工作:我们找到重命名,将所有文件合并为动词,然后进行普通的非合并提交,即E'.
最后,与非交互式 rebase 一样,Git 将分支名称从旧提示提交中剥离,并将其放在新提示上。
使用git format-patch. 最重要的是,git format-patch字面上不能产生“空”补丁——一个不对源进行任何更改的提交——所以如果你-k习惯“保留”这样的提交,非交互式 rebase 使用git cherry-pick.
第二个是因为git format-patch被告知--no-renames(参见上面的实际命令),它表示文件重命名为“删除旧文件,添加新文件”。这可以防止 Git 发现一些冲突。(只要待删除的文件在补丁中,它至少可以检测到删除/修改冲突,但不能检测到删除/重命名冲突,而在“超出”重命名的补丁中,它会什么都没有注意到。),当然,如果我们能够建立在一个修补程序适用,因为情况很明显-有效范围内,即使三方合并可能会发现匹配的情况下是从移动复制的代码,我们可以成功地应用一个补丁,其中三路合并要么检测冲突,要么在其他地方应用它。
(我打算在某个时候构建一个示例,但一直没有时间去做。)
如果你使用这个-m选项,指定 rebase 应该使用合并机制,或者一个-s <strategy>选项 or -X <extended-option>(两者都暗示使用合并机制),这也会强制 Git 使用cherry-pick。然而,这实际上是第三种变基!
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
Run Code Online (Sandbox Code Playgroud)
请注意,隐藏状态文件的位置,跟踪您是否正处于git rebase停止以让您编辑(交互式变基)或由于冲突(任何变基)而停止的正在进行中,这取决于变基的类型.
最后一点不同是am基于 rebase 不运行git notes copy。其他两个可以。这意味着您在原始提交上所做的注释在使用时会被删除git rebase,但在使用交互式 rebase 或git rebase -m.
(这对我来说似乎是一个错误,但也许是故意的。保存笔记会有点棘手,因为我们需要从旧提交哈希到新提交哈希的映射。这需要内部支持git am。)