(编辑,2016年11月30日:另请参见此答案,以回答为什么git rebase会丢弃我的提交?。 现在几乎可以确定这是由于 fork-point 选项引起的。)
手动和基于pull
的git rebase
之间确实有一些差异(在比--fork-point
选项出现在git merge-base
之前的git版本中更少),而且我怀疑您的自动保留合并可能会涉及其中某些差异。虽然很难确定,但您的本地分支跟随正在进行变基的另一个本地分支的事实是相当有启示性的。与此同时,旧版git pull
脚本也最近被重写成了C,因此很难看出它具体执行了什么操作(尽管您可以将环境变量GIT_TRACE
设置为1
,以便让git在内部运行命令时显示给你)。
无论如何,在这里有两个或三个关键项目(取决于您如何计数和拆分这些项目,我将其拆分为3):
git pull
运行git fetch
,然后根据指令运行git merge
或git 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
其中A
和B
是您在创建分支时的提交记录(使B
成为合并基础),C
到E
是您通过git fetch
从远程拾取的新提交记录,而I
到K
则是您自己的提交记录。重定位代码将复制I
到K
,并将第一个副本附加到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'
替换了,但我们希望它不会影响I
到K
的内容。此时起点看起来像这样:
B' - C - D - E <-- origin/foo
/
... - A - B <-- [origin/foo@{n}]
\
I - J - K <-- foo
使用
git rev-list origin/foo..foo
,我们将选择提交
B
、
I
、
J
和
K
进行复制,并像往常一样尝试将它们粘贴到
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-name
是 origin/master
(或者说是 refs/remotes/origin/master
),第二次变基的 upstream-name
是 master
(refs/heads/master
)。
这个过程中应该都能正常工作。如果整个过程开始时你的提交图看起来像你所描述的那样:
... - A - B <-- master, origin/master
\
I - J - K <-- feature
然后第一个 fetch
带来一些提交并使 origin/master
指向新的末端:
C - D - E <
/
... - A - B <
\
I - J - K <
第一次变基操作没有要复制的内容(master
和B
的合并基点——B
=fork-point(master, origin/master)——就是B
,因此没有什么可以复制的),结果为:
C - D - E <
/
... - A - B <
\
I - J - K <
第二次提取是从自己进行的,并且完全是无操作/跳过的,因此将其作为第二个变基的输入保留。
--onto
目标是
master
,即提交
E
,
HEAD
(
feature
) 和
master
的分叉点也是提交
B
,通常留下提交
I
到
K
在提交
E
之后复制。如果有一些提交被删除了,在这个过程中可能出了问题,但我看不到是什么问题。
origin/master
进行变基,而是尝试将其向前推进,而不影响你的(跟踪)本地副本。我还没有检查正确调用的手册,但也许可以在一个新的临时名称下 checkout -b 那个分支,然后将该临时分支变基到你当前的 HEAD。 - Philip Oakleyorigin/master
,而是在将当前分支master
变基到origin/master
上。根据我的理解,这应该基本上将HEAD
带到origin/master
的末端,并重新应用在分支上的master
末端的提交。基本上,将master
上的新提交重写为如果它们发生在来自origin/master
的更改之后。 - ashaysgit pull
存在自动分叉点重定基错误,知道版本可能会使检查变得更容易一些。 - torek