在实际团队开发中,我们通常会在自己的功能分支上进行开发,完成后需要将代码合并回主分支。Git提供了两种主流的合并方式:merge和rebase。它们都能达到合并代码的目的,但背后的原理和最终产生的提交历史却截然不同。本文将通过一个具体的开发场景,带你直观地理解两者的核心区别。
场景搭建:一个简单的项目
首先,我们在Git上新建一个项目(例如 rebaseStudy),默认会有一个 master 分支。将项目克隆到本地,准备工作就完成了。

同学A:在主分支提交
同学A在本地执行 git log,可以看到初始的提交记录。
lianjia@DESKTOP-B7ANVOH MINGW64 /d/wilson/rebaseStudy (master) $ git log
commit 71dbd8a5c376d574ad00b32cc8ebb7c7f87f16e (HEAD -> master, origin/master, origin/HEAD)
Author: shuyuan1992 <42328986+shuyuan1992@users.noreply.github.com>
Date: Sun Nov 11 10:07:08 2018 +0800
Initial commit
接着,他新增一个文件 a.txt,提交后再次查看提交记录,现在有两条了。
lianjia@DESKTOP-B7ANVOH MINGW64 /d/wilson/rebaseStudy (master)$ git log
commit zae743b22fe94c636c7cfae192b9ff34ceed138 (HEAD -> master)
Author: wilson <weishuyuan001>
Date: Sun Nov 11 10:24:25 2018 +0800
A first commit a.txt
commit 71dbd8a5c376d574ad00b32cc88ebb7c7f87f16e (origin/master, origin/HEAD)
Author: shuyuan1992 <42328986+shuyuan1992@users.noreply.github.com>
Date: Sun Nov 11 10:07:07 2018 +0800
Initial commit
将本次新的提交推送到远程仓库后,在GitHub上就能看到这两次提交记录了。

同学B:创建功能分支并开发
此时,同学B基于最新的 master 分支(提交点 C2)检出一个新的功能分支 dev,并推送到远程。
Tianjia@DESKTOP-B7ANV0H MINGW64 /d/wilson/rebaseStudy (master) $ git checkout -b dev
Switched to a new branch ‘dev’
Tianjia@DESKTOP-B7ANV0H MINGW64 /d/wilson/rebaseStudy (dev) $ git push origin dev
Total 0 (delta 0), reused 0 (delta 0)
remote:
remote: Create a pull request for ‘dev’ on GitHub by visiting:
remote: https://github.com/shuyuan1992/rebaseStudy/pull/new/dev
To https://github.com/shuyuan1992/rebaseStudy.git
* [new branch] dev -> dev
Tianjia@DESKTOP-B7ANV0H MINGW64 /d/wilson/rebaseStudy (dev) $ git branch
dev
* master
远程仓库随之多出了一个 dev 分支。

此时,Git 的分支状态如下图所示,dev 和 master 指向同一个最新的提交 C2。

随后,B同学在 dev 分支上进行了三次功能开发提交。查看日志如下:
Tianji@DESKTOP-B7ANV0H MINGW64 /d/wilson/rebaseStudy (dev) $ git log
commit 5c0adc3677504f8db8cc94dc89a415356aa2e97 (HEAD -> dev)
author: wilson <weishuyuan001>
Date: Sun Nov 11 10:57:54 2018 +0800
A 4 commit a.txt
commit 6d860217c834da6a9ebc2fc7e30dfdc788fb763e
author: wilson <weishuyuan001>
Date: Sun Nov 11 10:57:15 2018 +0800
A 3 commit a.txt
commit 89f2cda3eda29ead5b244de96d61e12467741d7
author: wilson <weishuyuan001>
Date: Sun Nov 11 10:56:42 2018 +0800
A 2 commit a.txt
commit 2ae743b22fe94c636c7cfaee192b9ff34ceed138 (origin/master, origin/dev, origin/HEAD, master)
Author: wilson <weishuyuan001>
Date: Sun Nov 11 10:24:25 2018 +0800
A first commit a.txt
commit 71bdb5c376d574ad00b32cc88ebb7c7f87f16e
Author: shuyuan1992 <42328986+shuyuan1992@users.noreply.github.com>
Date: Sun Nov 11 10:07:07 2018 +0800
Initial commit
此时的分支状态图变为一条“分叉”的形态,dev 分支领先于 master 分支三个提交(C3, C4, C5)。

关键问题:主分支更新了
现实开发中常见的情况是:当B同学在 dev 分支开发时,其他同学(比如A同学)向 master 分支合并了新的功能。也就是说,master 分支已经向前推进了,产生了新的提交 C6。
此时的分支状态图如下,dev 分支是基于旧的 master(C2)开发的,而 master 已经更新到了 C6。

现在,B同学完成了 dev 分支的开发,需要将其功能合并到 master 分支。他面临着两种选择:git merge 或 git rebase。
方案一:使用 git merge 合并
如果B同学选择直接执行 git merge,Git 会进行以下操作:
- 找到
master 和 dev 两个分支的最近共同祖先,即提交 C2。
- 将
dev 分支的最新提交 C5 和 master 分支的最新提交 C6 进行合并,如果存在冲突需要解决。这个操作会产生一个全新的合并提交 C7。
- 最终,将 C2 之后
dev 和 master 的所有提交点,按照提交时间顺序整合到 master 分支的历史中。
合并后的分支状态图如下,产生了一个新的合并提交 C7,历史轨迹呈现出非线性的“分叉-合并”结构。

方案二:使用 git rebase 变基
如果B同学希望得到一条更整洁的线性历史,他可以选择 rebase(变基)。
- 首先,切换到需要变基的分支,即
dev 分支。
- 执行
git rebase master。这条命令的语义是:“将当前 dev 分支的修改,在最新的 master 分支的基础上重新‘播放’一遍”。
- Git 会暂时保存
dev 分支上 C2 之后的所有提交(C3, C4, C5),然后找到 master 分支的最新提交 C6,并以此为新的基础。
- 接着,Git 会将保存的那些提交依次应用到 C6 之后。这个过程可能会产生冲突,需要解决。解决后使用
git add . 和 git rebase --continue 继续。
- 最终,
dev 分支的起点从 C2 变为了 C6,原来的提交 C3, C4, C5 会被复制成新的提交 C3', C4', C5'(哈希值改变),从而形成一条线性的历史。
变基后的分支状态图如下所示。可以看到,整个 master 分支并没有多出一个合并提交,历史是一条干净的直线。原来 dev 分支上的几次提交内容被保留,但它们的哈希值已经改变。

核心总结与选择建议
git merge:保留真实的合并历史,会创建一个新的合并提交(Merge Commit)。最终的分支树呈现非线性的结构,清晰地记录了分支何时合并以及为何合并。这是保留完整项目历史的推荐方式。
git rebase:重写提交历史,将当前分支的提交“移植”到目标分支的最新提交之后。结果是得到一条线性的分支历史,更加整洁。但这也意味着改写了公共历史,适用于整理本地、尚未推送的提交。
如何选择?
一个被广泛接受的准则是:只对尚未推送的本地提交执行变基(rebase),永远不要对已推送至远程仓库的提交进行变基。 对于需要整合到公共分支(如 master)的更改,使用 merge 更为安全,因为它不会改变其他开发者所依赖的公共历史。
理解 merge 和 rebase 的差异,能帮助你在不同的团队协作场景下做出合适的选择,从而维护一个清晰且实用的项目提交历史。如果你想与更多开发者交流此类版本控制的最佳实践,欢迎在技术社区中参与讨论。