这个问题如果有一个具体的例子会更容易回答。但原则上,
git rebase
意味着“复制提交记录”,就像使用
git cherry-pick
一样。在这里失败的是复制步骤。
我们从分支
R(重新设置)开始,并选择一些新的目标分支
T(目标)。然后,我们确定要复制的一组提交:这些提交大多数情况下是在
R 分支上但不在
T 分支上的提交。(在稍微有点谎言的情况下,
git rebase 文档 表示,这些是由
git log T..R
或
git log T..HEAD
列出的提交。这基本上是正确的,除了添加了三个额外的小把戏,或者说“减去”可能是正确的词。)
然后,在列出要复制的提交哈希值之后,Git 对目标分支进行分离 HEAD checkout,以便当前的(
HEAD
)提交与分支
T 标识的提交相同。请注意,我们没有处于分支
T 上,我们只是在同一个提交上。现在,对于每个保存了哈希 ID 的提交,Git 有效地(有时也确实)运行
git cherry-pick <hash-id>
:
....... HEAD
v
...--o--o--o--o--o--o <-- T
\
A--B--C <-- R
我们运行
git cherry-pick A
来复制提交
A
。如果我们将新副本称为
A'
,则结果如下:
A' <-- HEAD
/
...--o--o--o--o--o--o <-- T
\
A--B--C <-- R
然后我们运行git cherry-pick B
将B
复制到B'
:
A'-B' <-- HEAD
/
...--o--o--o--o--o--o <-- T
\
A--B--C <-- R
我们重复以上操作,直到所有提交都被复制,然后作为最后一步,Git将“分支标签”
R
从原始链中“剥离”,并将其粘贴在已复制的链的末尾。
A'-B'-C' <-- R (HEAD)
/
...--o--o--o--o--o--o <-- T
\
A--B--C <-- (only in reflogs and ORIG_HEAD)
冲突和其他问题
每个副本——每个挑选步骤——都是使用Git的合并机制完成的,或者我称之为“合并”动词形式(与名词或形容词形式相反,即合并或合并提交——git merge
通常在执行动词形式的合并后生成一个合并提交,而我们只是使用了这个提交的前半部分)。例如,在合并添加新文件的提交时,如果也添加相同的新文件的更改,则会出现增加/增加冲突。
最可能发生这种冲突的地方是从A
创建拷贝A'
,因为挑选操作的合并基础是要复制的提交的父提交。 合并操作——“合并”动词——将此合并基础与两个提交进行比较。 在这种情况下,这两个提交是提交A
本身和分支T的最高提交,即T指向的提交。 让我们称其为X
,并将合并基础用*
标记:
? [merge in progress] HEAD
/
...--o--o--*--o--o--X <-- T
\
A--B--C <-- R
如果提交记录
A
和
X
相对于
*
都添加了文件
path/to/file.txt
,那么就会出现一个add/add冲突。此时cherry-pick会停止,并留下一个合并冲突。你需要解决它并告诉rebase继续执行。
如果
A
添加了
path/to/file.txt
,但在提交
X
中没有该文件,则通常不会有问题:Git只需创建该文件并将其放入新提交中。根本不会出现add/add冲突,只会生成完美的副本
A'
(然后我们会继续进行rebase的其余部分)。
但是,如果某些原因(例如,作为未跟踪的文件并且可能被忽略)工作目录中存在文件
path/to/file.txt
,那么Git不能简单地将
path/to/file.txt
从提交
A
中复制到工作目录中覆盖掉已有的版本。这时会出现错误消息,就像普通的合并冲突一样。你必须解决这个问题并告诉rebase继续执行。
对于一些在索引/暂存区中的文件(因此可以在各种提交中),但被标记为
--assume-unchanged
或
--skip-worktree
的文件,这里还有一些额外的情况。在较新版本的Git中,精确的错误消息会标识“将被覆盖”的文件是否真正为未跟踪文件或标记为此类文件。 (这在旧版本的Git中可能也是正确的,但我没有查过。)这就是为什么具体的示例更好:这个特定问题有几个不同的原因。
为了完整起见:我提到的减法
这些也在rebase文档中有所涉及,但值得提到
git rebase
故意不复制的提交:
不能复制合并操作。使用--preserve
参数时,git rebase
将重新执行合并操作以尝试重建它们,但它无法复制原始的合并操作。因此通常它甚至不会尝试。
省略已有上游等效的合并操作(例如那些已经被cherry-pick的)。在原始图表中(包括T和R),Git会检查A
、B
或C
的git patch-id值是否与合并基准提交右侧的任何提交的git patch-id值匹配。如果是,则Git会放弃patch ID已经存在于上游的A
、B
和/或C
提交。
如果与--fork-point
一起使用,则重新定位代码运行git merge-base --fork-point
来尝试找到已删除的上游提交。这通常对远程跟踪分支非常有效,但如果您将自己的本地分支设置为自己分支的上游,则通常效果较差。在使用自动机制在上游进行变基时,默认使用--fork-point
参数,因此在这里必须谨慎。有关更多信息,请参见Git rebase - commit select in fork-point mode。