Git rebase 和 git merge 之间有区别吗

据我所知,他们都帮助我们得到一个线性的历史。

根据我的实验,rebase 一直都有效。但是合并—— ff-只在可以快进的场景中有效。

我还注意到,git merge 创建了一个合并提交,但是如果我们使用—— ff-only,它会提供一个线性历史记录,基本上等同于 git rebase。所以,只会扼杀 Git 合并的目的,对吧?

那么它们之间的实际区别是什么呢?

73794 次浏览

是的,有区别。如果不能快进,git merge --ff-only将中止,并接受一个提交(通常是一个分支)来合并。它只会在不能快进的情况下创建一个合并提交(也就是说,在 --ff-only中永远不会这样做)。

git rebase在当前分支上重写历史记录,或者可以用于将现有分支重新基于现有分支。在这种情况下,它不会创建合并提交,因为它是重新定基,而不是合并。

是的,在普通 git merge失败的情况下,--ff-only总是会失败,而在普通 git merge成功的情况下,--ff-only可能会失败。这就是问题的关键——如果你试图保持一个线性的历史,并且合并不能这样做,你 想要它失败了。

将故障情况添加到命令中的选项并非毫无用处; 它是验证先决条件的一种方法,因此,如果系统的当前状态与您预期的不同,那么就不会使问题变得更糟。

请注意,git rebasegit merge有不同的 工作(有或没有 --ff-only)。rebase所做的就是接受现有的提交并对其进行 收到。例如,假设您在 branch1上,并且两次提交了 AB:

...-o--o--A--B   <-- HEAD=branch1
\
o--C    <-- branch2

然后决定将这两个提交放在 branch2上。您可以:

  • 获取在 A中所做的更改的列表(与其父代的 A不同)
  • 获取在 B中所做的更改的列表(区分 BA)
  • 切换到 branch2
  • 做出与在 A中相同的更改并提交它们,从 A复制提交消息; 我们将其称为提交 A'
  • 然后进行与在 B中相同的更改并提交它们,从 B复制提交消息; 我们将其称为 B'

有一个 git 命令可以为您执行 diff-and-then-copy-and-commit 操作: git cherry-pick:

git checkout branch2      # switch HEAD to branch2 (commit C)
git cherry-pick branch1^  # this copies A to A'
git cherry-pick branch1   # and this copies B to B'

现在你有了这个:

...-o--o--A--B         <-- branch1
\
o--C--A'-B'   <-- HEAD=branch2

现在你可以切换回 branch1,使用 git reset删除原来的 AB(我在这里使用 --hard,这样更方便,因为它也清理了工作树) :

git checkout branch1
git reset --hard HEAD~2

这删除了原来的 AB1,所以现在你有:

...-o--o               <-- HEAD=branch1
\
o--C--A'-B'   <-- branch2

现在,您只需要重新签出 branch2来继续在那里工作。

这就是 git rebase所做的: 它“移动”提交(尽管不是通过实际移动它们,因为它不能: 在 git 中,提交永远不能更改,所以即使只是更改父 ID 也需要将其复制到 new 和略有不同的提交)。

换句话说,虽然 git cherry-pick提交的自动差分和重做,但 git rebase是重做 多个提交的自动过程,最后移动标签来“忘记”或隐藏原始内容。

上面的例子说明了如何将提交从一个本地分支 branch1移动到另一个本地分支 branch2,但是当执行 git fetch(包括 fetch,这是 git pull的第一步)时,当远程跟踪分支获得一些新的提交时,git 使用 完全相同的过程来移动提交。您可以从 feature分支开始,它拥有 origin/feature的上游,然后您可以自己提交一些内容:

...-o        <-- origin/feature
\
A--B   <-- HEAD=feature

但是之后你决定你应该看看上游发生了什么,所以你运行 git fetch2,啊哈,上游有人写了一个提交 C:

...-o--C     <-- origin/feature
\
A--B   <-- HEAD=feature

在这一点上,你可以简单地将你的 featureAB重新定位到 C上,给出:

...-o--C     <-- origin/feature
\
A'-B'  <-- HEAD=feature

这些是你原来的 AB的复制品,复制品完成后,原件会被扔掉(但请参阅脚注1)。


有时候没有什么需要重新定位的,也就是说,没有你自己做的工作。也就是说,fetch之前的图像是这样的:

...-o      <-- origin/feature
`-- HEAD=feature

然而,如果您接着进行 git fetch和提交 C,那么只剩下指向旧提交的 你的 feature分支,而 origin/feature已经向前移动:

...-o--C   <-- origin/feature
`---- <-- HEAD=feature

这就是 git merge --ff-only发挥作用的地方: 如果您要求将当前的分支 featureorigin/feature合并,git 会发现可以像以前那样将箭头向前滑动,这样 feature就可以直接指向提交 C。不需要实际合并。

如果你有自己的提交 AB,但是,你要求合并这些与 C,git 将做一个真正的合并,使一个新的合并提交 M:

...-o--C        <-- origin/feature
\   `-_
A--B--M   <-- feature

在这里,--ff-only将停止并给你一个错误。另一方面,Rebase 可以将 AB复制到 A'B',然后隐藏原来的 AB

所以,简而言之(好吧,太晚了: ——) ,他们只是做不同的事情。有时候结果是一样的,有时候不一样。如果可以复制 AB,那么可以使用 git rebase; 但是如果 没有有很好的理由复制它们,那么可以使用 git merge,也许可以使用 --ff-only,来进行合并或失败。


Git 实际上将原件保存了一段时间(在这种情况下通常是一个月) ,但它将原件隐藏了起来。找到它们最简单的方法是使用 git 的“ relog”,它保存每个分支指向的位置和 HEAD指向的位置的历史记录,在每次更新分支和/或 HEAD之前。

最终,reflog 历史记录条目过期,此时这些提交将符合 垃圾回收的条件。

2 或者,您可以再次使用 git pull,这是一个方便的脚本,从运行 git fetch开始。一旦获取完成,方便脚本将运行 git mergegit rebase,这取决于您如何配置和运行它。