ChatGPT解决这个技术问题 Extra ChatGPT

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

从我读到的内容来看,它们都帮助我们获得了线性历史。

根据我的实验,rebase 一直有效。但是 merge --ff-only 只适用于可以快速转发的场景。

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

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


a
aquaraga

请注意,git rebasejobgit merge 不同(有或没有 --ff-only)。 rebase 所做的是获取现有的提交并复制它们。例如,假设您在 branch1 上并进行了两次提交 AB

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

并且您决定宁愿将这两个提交放在 branch2 上。你可以:

获取您在 A 中所做的更改列表(差异 A 与其父项)

获取您在 B 中所做的更改列表(差异 B 与 A)

切换到分支 2

进行您在 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

这将删除原来的 AB,1,所以现在您有:

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

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

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

换句话说,虽然 git cherry-pickone 提交的自动 diff-and-redo,但 git rebase 是重做 multiple 提交的自动过程,此外,在最后,移动标签以“忘记”或隐藏原件。

上面说明了将提交从一个本地分支 branch1 移动到另一个本地分支 branch2,但是当您有一个获取一些新提交的远程跟踪分支时,git 使用 完全相同的过程来移动提交当您执行 git fetch(包括作为 git pull 的第一步的 fetch)时。您可以从具有 origin/feature 上游的分支 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 的更改之前记录每个分支指向的位置和 HEAD 指向的位置。

最终,reflog 历史条目过期,此时这些提交符合 garbage collection 的条件。

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


很好的答案。谢谢。
我喜欢您使用 git cherry-pick 来解释 git rebase 的方式。看起来很棒!
当您将 PR 从开发分支合并到 master(prod) 时,推荐的策略是什么?
@ShanikaEdiriweera:由谁推荐,出于什么目的? (我去过的各个地方都有不同的目标,因此使用了不同的机制。)在不了解您的情况的情况下,我无法提供有用的建议。
这听起来像是一个进行标准合并的地方,这里可能也有 --no-ff
a
abligh

是,有一点不同。如果 git merge --ff-only 不能快进,它将中止,并接受一个提交(通常是一个分支)来合并。如果它不能快进,它只会创建一个合并提交(即永远不会使用 --ff-only)。

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


也就是说,--ff-only 会检查并在不重写历史的情况下无法合并。而 rebase 是通过重写历史来实现的。是吗?
不完全的。合并不会重写历史,因此 git merge 永远不会重写历史(有或没有 --ff-only)。 --ff-only 阻止除快进合并以外的任何合并(没有合并重写历史记录)。除了快进合并之外的合并通常只是无序地应用变更集(这是一种简化,但足够好),加上记录变更集已合并的元数据;不涉及历史重写。
佚名

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

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


是不是这样,--ff-only 用于验证场景并在可能的情况下创建线性历史;而 rebase 更像是创建一个线性历史,而不管场景如何。是吗?
是的,如果您对某些不够干净的内容进行了重新设置,merge --ff-only 您将从历史树中的一个位置删除提交,然后将它们(带有修改)重新插入到另一个位置。这称为“重写历史”,如果您在未发布的分支上,这很好,但是在您发布了您的分支并且其他人克隆了它之后,重写历史会给他们带来问题。