git rebase 和 git merge --ff-only 有区别吗

Is there a difference between git rebase and git merge --ff-only

根据我的阅读,它们都帮助我们获得了线性历史。

根据我的实验,变基一直有效。但是merge --ff-only只在可以快进的场景才有效

我也注意到,git merge 创建了一个合并提交,但是如果我们使用 --ff-only,它给出了一个线性历史,这基本上等于 git 变基。所以 --ff-only 扼杀了 git 合并的目的,对吧?

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

是的,有区别。 git merge --ff-only 如果不能快进,将中止,并进行提交(通常是分支)合并。如果不能快进,它只会创建一个合并提交(即永远不会这样做 --ff-only).

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

是的,--ff-only 总是会在普通 git merge 失败的地方失败,并且可能会在普通 git merge 成功的地方失败。这就是重点 - 如果您试图保持线性历史记录,并且不能以这种方式进行合并,那么您希望它失败。

将失败案例添加到命令的选项并非无用;这是一种验证先决条件的方法,因此如果系统的当前状态不是您所期望的,您不会使问题变得更糟。

请注意 git rebase 工作 git 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 命令可以为您执行此差异然后复制并提交: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

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

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

现在您只需重新签出 branch2 即可继续在那里工作。

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

换句话说,git cherry-pickone 提交的自动差异重做,git rebase 是重做 multiple commits, plus, at the end, 移动标签以“忘记”或隐藏原件。

上面说明了将提交从一个本地分支 branch1 移动到另一个本地分支 branch2,但是 git 使用 完全相同的过程 当您有一个远程跟踪分支时移动提交,该分支在您执行 git fetch 时获取一些新提交(包括 fetch,这是 git pull 的第一步)。您可以从分支 feature 开始,该分支具有 origin/feature 的上游,并进行一些您自己的提交:

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

但是你决定你应该看看上游发生了什么,所以你 运行 git fetch,2 啊哈,上游有人写了一个提交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 一起使用,以适当地合并或失败。


1Git 实际上会保留原件一段时间——在这种情况下通常是一个月——但它会将它们隐藏起来。找到它们的最简单方法是使用 git 的“reflogs”,它保留了每个分支指向的位置的历史记录,以及 HEAD 指向的位置,在更新分支 and/or 的每个更改之前HEAD.

reflog 历史条目最终会过期,此时这些提交有资格获得 garbage collection

2或者,同样,您可以使用 git pull,这是一个以 运行ning git fetch 开头的便捷脚本。获取完成后,便捷脚本 运行 会 git mergegit rebase,具体取决于您如何配置和 运行 它。