理解 "git pull --rebase" 和 "git rebase" 的区别

9
根据我对git pull --rebase origin master的理解,它应该相当于运行以下命令:
(from branch master):  $ git fetch origin
(from branch master):  $ git rebase origin/master

看起来我发现了一些情况,即此处的操作与预期不符。在我的工作区中,我有以下设置:

  • 分支origin/master引用远程origin上的master分支。
  • master分支被设置为跟踪origin/master,并且落后于主分支几个提交。
  • feature分支被设置为跟踪本地master分支,并且领先于master几个提交。

有时,运行以下步骤会导致我失去提交记录:

(from branch master):  $ git pull --rebase
(from branch master):  $ git checkout feature
(from branch feature): $ git pull --rebase

目前,我在feature分支上进行的一些提交已经丢失了。现在,如果我重置我的位置,然后执行以下操作:

(from branch feature): $ git reset --hard HEAD@{2} # rewind to before second git pull
(from branch feature): $ git rebase master

提交已经被正确应用,而我在feature上的新提交仍然存在。这似乎直接与我对git pull工作原理的理解相矛盾,除非git fetch .做出了比我预期更奇怪的事情。
不幸的是,这并非所有提交都可以100%重现。但是,每次它成功运行时,它都会有效。
注意:这里的git pull --rebase实际上应该被理解为一个--rebase=preserve,如果有影响的话。我在~/.gitconfig中有以下配置:
[pull]
    rebase = preserve

你不应该对远程跟踪分支 origin/master 进行变基,而是尝试将其向前推进,而不影响你的(跟踪)本地副本。我还没有检查正确调用的手册,但也许可以在一个新的临时名称下 checkout -b 那个分支,然后将该临时分支变基到你当前的 HEAD。 - Philip Oakley
1
我认为这里有些混淆。我不是在变基origin/master,而是在将当前分支master变基到origin/master上。根据我的理解,这应该基本上将HEAD带到origin/master的末端,并重新应用在分支上的master末端的提交。基本上,将master上的新提交重写为如果它们发生在来自origin/master的更改之后。 - ashays
你的本地 Git 版本是什么?我问这个问题是因为我记得(有点模糊,还没有回去检查)在几个 2.x 版本中 git pull 存在自动分叉点重定基错误,知道版本可能会使检查变得更容易一些。 - torek
好的,所有已知的错误都已经在早期实质性地解决了。我会提供一个关于我所知道的差异的答案,但我不认为这会对这里有多大帮助。 - torek
@ashays,抱歉让你感到困惑。我想表达的关键点(除了错误之外)是要使用rebase的三个引用版本“rebase --onto”将其变基到正确的位置 - 这在man页面中有详细说明... - Philip Oakley
显示剩余2条评论
1个回答

12

(编辑,2016年11月30日:另请参见此答案,以回答为什么git rebase会丢弃我的提交?。 现在几乎可以确定这是由于 fork-point 选项引起的。)

手动和基于pullgit rebase之间确实有一些差异(在比--fork-point选项出现在git merge-base之前的git版本中更少),而且我怀疑您的自动保留合并可能会涉及其中某些差异。虽然很难确定,但您的本地分支跟随正在进行变基的另一个本地分支的事实是相当有启示性的。与此同时,旧版git pull脚本也最近被重写成了C,因此很难看出它具体执行了什么操作(尽管您可以将环境变量GIT_TRACE设置为1,以便让git在内部运行命令时显示给你)。

无论如何,在这里有两个或三个关键项目(取决于您如何计数和拆分这些项目,我将其拆分为3):

  • git pull运行git fetch,然后根据指令运行git mergegit rebase,但当它运行git rebase时,它使用新的 fork-point 机制来“从上游变基中恢复”。

  • 当不带参数运行git rebase时,它具有一种特殊情况,会调用 fork-point 机制。当使用参数运行时,只有在显式请求时才启用 fork-point 机制,并且该选项为 --fork-point

  • 当指示git rebase保留合并时,它使用交互式变基代码(以非交互方式)。我不确定这在这里是否真的很重要(因此上面说“可能涉及”)。通常,它会折叠掉合并,而只有交互式变基脚本才有代码来保存它们(实际上重新执行合并,因为没有其他方法来处理它们)。

最重要的项目(确切的)是 fork point 代码。该代码使用 reflog 来处理最好通过绘制部分提交图表显示的情况。

在正常(不需要 fork point 的)变基情况下,您将得到类似于以下内容:

... - A - B - C - D - E   <-- origin/foo
            \
              I - J - K   <-- foo

其中AB是您在创建分支时的提交记录(使B成为合并基础),CE是您通过git fetch从远程拾取的新提交记录,而IK则是您自己的提交记录。重定位代码将复制IK,并将第一个副本附加到E,第二个副本附加到I的副本,第三个副本附加到J的副本。

Git会—至少以前是这样—使用git rev-list origin/foo..foo找到要复制的提交记录,即使用当前分支的名称(foo)查找K并向上工作,使用其上游分支的名称(origin/foo)查找E并向上工作。向后查找会停止在合并基础处,本例中为B,而复制的结果如下:

... - A - B - C - D - E   <-- origin/foo
           \            \
            \             I' - J' - K'   <-- foo
             \
              I - J - K   [foo@{1}: reflog for foo]

当上游分支——这里是origin/foo——被变基时,该方法出现问题。例如,假设在origin上有人强制推送,以至于B被一个带有不同提交说明(可能还有不同的树)的新副本B'替换了,但我们希望它不会影响IK的内容。此时起点看起来像这样:

          B' - C - D - E    <-- origin/foo
        /
... - A - B   <-- [origin/foo@{n}]
            \
              I - J - K   <-- foo
使用git rev-list origin/foo..foo,我们将选择提交BIJK进行复制,并像往常一样尝试将它们粘贴到E之后;但我们不想复制B,因为它实际上来自origin并且已被其自己的副本B'替换。

分叉点代码的作用是查看origin的reflog,以查看B在某个时间是否可达。也就是说,它不仅检查origin/master(找到E,然后扫描回B',然后是A),还检查origin/master@{1}(直接指向B,可能取决于您多频繁地运行git fetch)、origin/master@{2}等等。从任何一个origin/master@{n}可达的foo上的提交都会被包括在寻找图中最小公共祖先节点的考虑范围内(也就是说,它们都被视为成为git merge-base打印出的合并基础的选项)。

(这里值得注意的是这里的一个缺陷:此自动分叉点检测只能找到在reflog条目保留的时间内可达的提交,在本例中默认为30天。但这与您的问题并不特别相关。)


在你的情况下,涉及三个分支名称(因此涉及三个reflog):

  • origin/master,由git fetch(在分支master上执行的git pull的第一步)更新
  • master,由你(通过普通提交)和git rebase(你的git pull的第二步)更新
  • feature,由你(通过普通提交)和git rebase(你的第二个git pull的第二步:你从自己“获取”,然后在master上对feature进行rebase)更新。

两次变基都使用 --preserve-merges(因此是非交互式模式),并且使用 --onto new-tip fork-point,其中通过运行 git merge-base --fork-point upstream-name HEAD 找到 fork-point 提交的 ID。第一次变基的 upstream-nameorigin/master(或者说是 refs/remotes/origin/master),第二次变基的 upstream-namemasterrefs/heads/master)。

这个过程中应该都能正常工作。如果整个过程开始时你的提交图看起来像你所描述的那样:

... - A - B   <-- master, origin/master
            \
              I - J - K   <-- feature

然后第一个 fetch 带来一些提交并使 origin/master 指向新的末端:

              C - D - E   <-- origin/master
            /
... - A - B   <-- master, origin/master@{1}
            \
              I - J - K   <-- feature

第一次变基操作没有要复制的内容(masterB的合并基点——B=fork-point(master, origin/master)——就是B,因此没有什么可以复制的),结果为:

              C - D - E   <-- master, origin/master
            /
... - A - B   <-- master@{1}, origin/master@{1}
            \
              I - J - K   <-- feature
第二次提取是从自己进行的,并且完全是无操作/跳过的,因此将其作为第二个变基的输入保留。 --onto 目标是 master,即提交 EHEAD (feature) 和 master 的分叉点也是提交 B,通常留下提交 IK 在提交 E 之后复制。如果有一些提交被删除了,在这个过程中可能出了问题,但我看不到是什么问题。

2
哇,这是一个很棒的答案,并且提供了许多关于这里发生情况的见解。我将花些时间尝试重现导致此问题的情况,并查看该答案是否能揭示一些信息。我相信这会直接引领我找到它,但我很想找到确切的原因。如果我有任何发现,我会告诉你的。 - ashays
在更深入地研究后,我无法始终重现用例(但我自然地再次找到了它)。这肯定是问题所在!感谢您花时间提供这个出色的答案。它确实增加了我对Git的理解。 - ashays
1
@ashays,导致您丢失提交记录的根本问题是什么? - michael.schuett
@mschuett(还有ashays):我在这里发布了一些具体的步骤来重现问题:https://dev59.com/an7aa4cB1Zd3GeqPnkD7#40886668 - 这可能有助于澄清如何丢失提交。 - Luke Usherwood
@LukeUsherwood:不错。我在另一个问题中放了一个链接到你的答案。 - torek
哇,谢谢大家!这真的有助于解释这里发生了什么。还要感谢@LukeUsherwood添加了重现步骤。 - ashays

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接