从技术上讲,我认为 Git 的做法有点愚蠢,
pull
脚本(一个 shell 脚本)应该为你执行这个操作。因此,你需要运行
git pull --rebase=preserve
而不是尝试使用
git pull --rebase --preserve-merges
。(或者,如我在
Vlad Nikitin's answer 的
a comment 中所指出的那样,你可以将
branch.name.rebase
设置为
preserve
来自动获取相同的效果。)
换句话说,你永远不应该运行
git pull --rebase --preserve-merges
,因为它会(错误地)将
--preserve-merges
传递给
fetch
步骤,而不是传递给
merge
或
rebase
步骤。但是,你可以运行
git pull --rebase=preserve
。
“何时(以及是否)使用任何类型的rebase,无论是保留合并还是不保留,这更多是一种观点问题。这意味着它在stackoverflow上并不适合。:-)
尽管如此,我会在这里提出一个声明:只有当您知道(在某种一般意义上)您正在做什么时,才应进行rebase
1,如果您确实知道自己在做什么,您可能通常会首选保留合并的rebase,尽管到您决定rebase是个好主意的时候,您可能会发现具有其自身嵌入式分支和合并点的历史记录不一定是正确的“最终重写历史”。
也就是说,如果适合进行rebase,那么被rebase的历史记录本身很可能是线性的,因此保留与否的问题无关紧要。”
编辑:添加绘图
这是提交图的一部分绘图,显示了两个命名分支mainline
和experiment
。 mainline
和experiment
的共同基础是提交节点A
,而mainline
有一个提交G
不在experiment
分支上:
...--o--A-------------G <-- mainline
\
\ .-C-.
B E--F <-- experiment
\_D_/
请注意,
experiment
分支也有一个分支和合并的过程:这两个分支的基础都是
B
,其中一个分支持有提交
C
,另一个分支则持有提交
D
。这两个未命名的分支在合并提交
E
时会收缩回单一的开发线,并且提交
F
位于合并提交之上成为
experiment
分支顶端。如果您在
experiment
分支上运行
git rebase mainline
,将会发生以下情况:
$ git rebase mainline
First, rewinding head to replay your work on top of it...
Applying: B
Applying: C
Applying: D
Applying: F
这是当前提交图中的内容:
...--o--A--G <-- mainline
\
B'-C'-D'-F' <-- experiment
在分支
experiment
上曾经有一个“结构性分支”,现在已经不存在了。
rebase
操作复制了我在提交
B
,
C
,
D
和
F
中所做的所有更改;这些更改成为新提交
B'
,
C'
,
D'
和
F'
。 (提交
E
是一个纯合并,没有更改,不需要复制。我还没有测试如果我重写嵌入更改的合并会发生什么,无论是为了解决冲突还是像一些人所说的那样进行“恶意合并”)。
另一方面,如果我这样做:
$ git rebase --preserve-merges mainline
[git grinds away doing the rebase; this takes a bit longer
than the "flattening" rebase, and there is a progress indicator]
Successfully rebased and updated refs/heads/experiment.
我得到了这张图表:
...--o--A--G <-- mainline
\
\ .-C'.
B' E'-F' <-- experiment
\_D'/
这样做保留了合并和实验的“内部分支”,这是好的吗?不好吗?还是无所谓?请阅读(非常长的)脚注!
1无论如何,学习“rebase”的作用都是一个好主意,而在git中(唉!)基本上需要在中等水平上学习“它是如何做到的”。基本上,rebase会复制(您早期更改的)提交,然后将其应用于(您或其他人的)后续提交,使其“看起来”像您以某种其他顺序完成了工作。一个简单的例子:两个开发人员,假设是Alice和Bob,都在同一个分支上工作。假设Marketing要求一个名为Strawberry的功能,Alice和Bob都在实现strawberry
的工作中,都在名为strawberry
的分支上。
Alice和Bob都运行git fetch
从origin
获取strawberry
。
Alice发现文件abc
需要一些更改来准备新功能。她写下并提交了更改,但还没有推送。
Bob编写了一个新功能的描述,更改了文件README
,但没有其他影响。Bob提交了他的更改并推送。
Alice随后更新文件feat
以提供实际的功能。她分别编写和提交了这个内容,现在准备推送。但是,糟糕的是,Bob先于她完成了这个操作:
$ git push origin strawberry
...
! [rejected] strawberry -> strawberry (non-fast-forward)
Alice应该获取更改并查看它们(不仅仅是盲目地合并或变基):
$ git fetch
...
$ git log origin/strawberry
(或者使用gitk
或任何其他工具 - 我自己倾向于使用git lola
,并根据需要使用git show
查看单个提交)。
从这个示例中可以看出,Bob只更改了README
,因此她的更改不会受到影响。此时,她可以确定安全地将自己的更改合并到origin/strawberry
上:
$ git rebase origin/strawberry
请注意,没有合并来保留,这使得它(在Git历史方面)看起来像是她先等待Bob更新文档,然后才开始实际实现更改 - 这些更改仍然分为两个单独的提交,以便稍后可以轻松地确定文件
abc
的更改是否会导致其他问题。但是,这两个单独的提交现在是相邻的,因此很容易看出更改
abc
的
目的是为了启用对文件
feat
的更改。由于更改
README
首先出现,因此更清楚这是更改
abc
的
目的。即使Alice只是:
$ git merge origin/strawberry
然而,这样会创建一个合并提交,其唯一的作用似乎是说“Alice在Bob更新README之前开始了abc,并在完成feat后结束”,这并不真正有帮助。
在更复杂的情况下,如果Bob不仅仅更新文档,Alice可能会发现最好将自己的提交(在这种情况下可能超过两个)重新排列成新的、不同的线性历史记录,以便Bob的某些更改(这次可能超过一个提交)“在中间”,例如,就像他们实时合作一样(谁知道,也许他们确实这样做了)。或者她可能会发现,最好将自己的更改保留为单独的开发线,与Bob的更改合并,甚至可能多次合并。
这完全取决于以后是否需要回顾(看起来的,如果是变基,或者实际的,如果不是)事件序列,为某些人提供最有用信息,可能是Alice和Bob,也可能是其他开发者。有时候每个单独的提交都是有用的信息。有时候重新排列和合并提交或删除某些提交更有用,例如证明是个坏主意的更改(但考虑保留它们以指出“这是个坏主意,所以将来不要再尝试”!)
git pull --rebase=merges
。 - marbel82