Git 之 merge 与 rebase 的区别

假设当前从 master 分别切出了两个分支:learn-rebase 和 learn-merge,它俩分别都进行了两次 commit,我们希望把两个分支的内容都合并到 master 上

clipboard.png

Merge(合并)

fast-forward

使用 merge 可以合并多个历史记录的流程。

首先我们合并 learn-merge 分支,只看蓝色的部分,learn-merge 分支与 master 分支的最新 commit 在同一条线上,这说明 learn-merge 分支的历史记录包含 master 分支所有的历史记录,那么这个合并是非常简单的。只需要通过把 master 分支的位置移动到 learn-merge 的最新分支上,Git 就会合并。这样的合并被称为 fast-forward(快进)合并。(fast-forward 模式下是不可能出现冲突的)

操作如下:

➜  git branch
  learn-merge
  learn-rebase
* master
➜  git merge learn-merge
Updating 594a6c9..c2b4ae3
Fast-forward
 myfile.txt | 2 ++
 1 file changed, 2 insertions(+)

结果如下:

clipboard.png

非 fast-forward

接下来需要合并 learn-rebase 分支,此时可以看到 master 的最新 commit 已经和 learn-merge 保持一致了,与 learn-rebase 分支不在一条直线上,那此时就不能进行 fast-forward 合并了。

在这种情况下使用 merge 方法,可以合并两个修改并生成一个新的提交。(此时可能会出现冲突的情况,因为 learn-rebase 分支可能与 learn-merge 分支修改同一个文件)

操作如下:

➜  git merge learn-rebase
Auto-merging myfile.txt
CONFLICT (content): Merge conflict in myfile.txt
Automatic merge failed; fix conflicts and then commit the result.
➜  vim myfile.txt # 解决冲突
➜  git add myfile.txt
➜  git commit -m 'fix conflicts'
[master 2a6899b] fix conflicts

结果如下:

clipboard.png

设置 non fast-forward

执行合并时,如果设定了non fast-forward 选项,即使在能够 fast-forward 合并的情况下也会生成新的提交并合并。

clipboard.png

设置方法:添加 -no-ff 选项

rebase(变基)

与 merge 会保留修改内容的历史记录不同,rebase 是在原有提交的基础上将差异内容反映进去。让我们回到 merge learn-rebase 之前的样子。

clipboard.png

rebase 的步骤分为两步,第一步将 learn-rebase 分支的历史记录添加在 master 分支的后面。这个时候可能会有冲突,当出现冲突时,解决冲突后的提交不是使用 commit 命令,而是执行 rebase 命令指定 --continue 选项。若要取消 rebase,指定 --abort 选项。

clipboard.png

操作如下:

➜  git checkout learn-rebase
Switched to branch 'learn-rebase'
➜  git rebase master
First, rewinding head to replay your work on top of it...
Applying: add rebase
Using index info to reconstruct a base tree...
M   myfile.txt
Falling back to patching base and 3-way merge...
Auto-merging myfile.txt
CONFLICT (content): Merge conflict in myfile.txt
error: Failed to merge in the changes.
Patch failed at 0001 add rebase
The copy of the patch that failed is found in: .git/rebase-apply/patch
When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
  1 merge 与 rebase 的差别
➜  vim myfile.txt # 解决冲突
➜  git add myfile.txt
➜  git rebase --continue
Applying: add a new line

结果如下:

clipboard.png

然后再使用 fast-forward 合并即可

➜  git checkout master
Switched to branch 'master'
➜  git merge learn-rebase
Updating 0b9e66f..1fc71b9
Fast-forward
 myfile.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

对比 merge 和 rebase 最终的历史记录,可以发现 merge 保持了修改内容的历史记录,但是历史记录会很复杂;而 rebase 后的历史记录简单,是在原有提交的基础上将差异内容反映进去。

clipboard.png

rebase 黄金定律

奇妙的变基也并非完美无缺,要用它得遵守一条准则:

不要对在你的仓库外有副本的分支执行变基。

变基操作的 实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。 如果你已经将提交推送至某个仓库,而其他人也已经从该仓库拉取提交并进行了后续工作,此时,如果你用 git rebase 命令重新整理了提交并再次推送,你的同伴因此将不得不再次将他们手头的工作与你的提交进行整合,如果接下来你还要拉取并整合他们修改过的提交,事情就会变得一团糟。

merge vs rebase

到底该使用 merge 方式开发还是使用 rebase 方法开发是有争议的

有一种观点认为,仓库的提交历史即是 记录实际发生过什么。 它是针对历史的文档,本身就有价值,不能乱改。 从这个角度看来,改变提交历史是一种亵渎,你使用谎言掩盖了实际发生过的事情。 如果由合并产生的提交历史是一团糟怎么办? 既然事实就是如此,那么这些痕迹就应该被保留下来,让后人能够查阅。

另一种观点则正好相反,他们认为提交历史是 项目过程中发生的事。 没人会出版一本书的第一版草稿,软件维护手册也是需要反复修订才能方便使用。 持这一观点的人会使用 rebase 及 filter-branch 等工具来编写故事,怎么方便后来的读者就怎么写。

总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史,从不对已推送至别处的提交执行变基操作,这样,你才能享受到两种方式带来的便利。

撤销提交的不同

如果使用 merge 进行合并,可以使用 revert 命令对 merge 的内容进行撤销操作(参考 revert),而使用 rebase 则不行(已经没有 merge commit 了),而需要使用 rebase -i 对提交进行重新编辑(参考 交互式 rebase)。

参考

阅读 21.7k

推荐阅读
活到老学到老
用户专栏

记录成长

34 人关注
27 篇文章
专栏主页