理解" git pull --rebase" vs" git rebase"

时间:2016-02-10 16:32:00

标签: git version-control rebase

根据我对git pull --rebase origin master的理解,它应该等同于运行以下命令:

(from branch master):  $ git fetch origin
(from branch master):  $ git rebase origin/master

我似乎找到了一些不按预期工作的情况。在我的工作区中,我有以下设置:

  • 分支origin/master引用远程master
  • 上的分支origin
  • 分支master设置为跟踪origin/master,并且通过多次提交隐藏在 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

1 个答案:

答案 0 :(得分:8)

(编辑,2016年11月30日:另见this answerWhy is git rebase discarding my commits?。现在几乎可以肯定它是由于fork-point选项。)

手动和基于pull的{​​{1}}之间存在一些差异(现在在2.7中比在git rebase选项之前的git版本中更少在--fork-point)。而且,我怀疑你的自动保留合并可能会涉及到。这有点难以确定,但是你的本地分支跟随你的另一个本地分支这一事实是非常具有启发性。同时,旧的git merge-base脚本最近也在C中重写,因此很难看到它的作用(尽管你可以将环境变量git pull设置为GIT_TRACE来制作git show你在内部运行它们的命令。)

在任何情况下,这里都有两到三个关键项目(取决于你如何计算和拆分它们,我将它变为3):

  • 1运行git pull,然后根据说明运行git fetchgit merge,但当它运行git rebase时,它会使用新的叉点从上游的转折中恢复的机器"。

  • git rebase运行时没有参数时,它有一个特殊情况,它调用fork-point机制。使用参数运行时,除非使用git rebase明确请求,否则将禁用fork-point机制。

  • 当指示--fork-point保留合并时,它使用交互式rebase代码(非交互式)。我不确定这在这里真的很重要(因此"可能涉及"上面)。通常它会使合并变平,只有交互式rebase脚本才能保存它们(这段代码实际上会重新进行合并,因为没有其他方法可以处理它们。)

这里最重要的项目(肯定)是叉点代码。此代码使用reflog来处理通过绘制提交图的一部分而最佳显示的案例。

在正常情况下(需要没有叉点的东西)rebase case你有这样的东西:

git rebase

其中... - A - B - C - D - E <-- origin/foo \ I - J - K <-- foo A是您启动分支时的提交(因此B是合并基础),BC是您通过E从远程选择的新提交,git fetchI是您自己的提交。 rebase代码将K复制到I,将第一个副本附加到K,将第二个副本附加到E,将第三个副本附加到副本 - 的 - I

Git计算出 - 或者习惯 - 承诺使用J进行复制,即使用当前分支的名称(git rev-list origin/foo..foo)来查找{ {1}}并向后工作,以及上游(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')。现在的出发点如下:

I

使用K,我们选择要复制的提交 B' - C - D - E <-- origin/foo / ... - A - B <-- [origin/foo@{n}] \ I - J - K <-- foo git rev-list origin/foo..fooBI,然后尝试粘贴它们像往常一样在J之后;但我们并非希望复制K,因为它实际上来自E,并已被其自己的副本B取代。

fork point代码的作用是查看origin的reflog,看看B'是否可以在某个时间到达。也就是说,它不仅检查origin(查找B并扫描回origin/master然后E),还检查B'(直接指向{{} 1}},可能取决于您运行A),origin/master@{1}的频率,等等。在B上可以从任何 git fetch访问的任何提交都包括在内,以便在图中找到最低公共祖先节点时考虑(即,它们全部被视为选项成为origin/master@{2}打印出来的合并基础。

(值得注意的是这里的缺陷:这种自动化的fork point检测只能找到在维护reflog条目时可以访问的提交,在这种情况下默认为30天。但是,这与您的问题不太相关。)

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

  • foo,由origin/master@{n}更新(git merge-base分行origin/master时的第一步)
  • git fetch,由您(通过正常提交)和git pullmaster的第二步)和
  • 更新
  • master,由您(通过正常提交)和git rebase第二个 git pull的第二步更新:您&#34 ;从你自己获取&#34;一个无操作,然后在feature上重新git rebase

两个rebase都使用git pull(因此是非交互式交互模式)和feature运行,其中 master 提交ID通过运行{{}找到1}}。第一个rebase的 --preserve-merges --onto new-tip fork-point(嗯,fork-point), git merge-base --fork-point upstream-name HEAD 为第二个rebase是upstream-nameorigin/master)。

这个应该所有Just Work。如果您在整个过程开始时的提交图表与您所描述的类似:

refs/remotes/origin/master

然后第一个upstream-name引入了一些提交,并使master指向新提示:

refs/heads/master

然后第一个rebase找不到要复制的内容(... - A - B <-- master, origin/master \ I - J - K <-- feature fetch的合并基础 - origin/master = fork-point(master,origin / master)-is just { {1}}所以没有什么可复制的),给出:

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

第二次获取来自你自己和完全没有操作/跳过,留下这作为第二个rebase的输入。 master目标为B,提交B,而B C - D - E <-- master, origin/master / ... - A - B <-- master@{1}, origin/master@{1} \ I - J - K <-- feature )和--onto的分叉也提交{{} 1}},让提交masterE照常复制HEAD

如果某些提交被删除,在此过程中出现问题,但我无法看到。