Git Merge VS Git Rebase: 如何优雅地合并分支?

有关 Git 的常用基本操作,我在👉 Git 基本使用这篇文章里已经介绍过了,这里不多赘述。我们今天主要讲讲 Git Merge 与 Git Rebase 的基础概念与使用场景。

概念解释

虽说两者都可以用来将一个分支(为了表达清晰我们暂且称之为待合并分支)合并到另一个分支(我们称之为目标分支或当前分支),但设计理念却是完全不同的。我们首先介绍两者的基本概念以及使用方法,在随后的使用场景中我们再来比较两者的区别。

首先需要知道,Git Merge 有两种常见的合并方式。

快进合并(Fast-forward merge)

此合并方式发生的条件为目标分支自创建出待合并分支后没有任何提交。合并过程可以简单理解为目标分支将待合并分支的多出来的提交记录补到自己的后面,并移动自己的HEAD指针到最后一个提交记录。

特点:

  • 保留两个分支的提交记录,并且提交记录为线性
  • 最简单的一种情况,不会发生冲突
  • 合并后的最后一次提交记录为待合并分支的提交,合并操作本身不会创建新的提交记录

举个栗子,场景如下:Dabby 从 main 分支的提交 Initial commit 拉取了 dev 分支,然后依次进行了提交 Add login 和 Fix bugs。

最后在 main 分支上执行 git merge dev 合并 dev 分支。

执行 git log --all --graph --oneline --decorate 查看拓扑结构如下(合并后):

普通合并(Recursive merge)

此合并方式发生的条件为目标分支自创建出待合并分支后目标分支和待合并分支都有提交。可以理解为是快进合并到升级版本。

特点:

  • 保留两个分支的提交记录
  • 可能会发生冲突
  • 合并操作本身会创建一个新的合并提交记录

举个🌰,场景如下:Dabby 从 main 分支的提交 Initial commit 拉取了 dev 分支,然后进行了提交 Fix bugs,与此同时,main 分支也创建了一个 Update 提交。

最后在 main 分支上执行 git merge dev -m "Merge" 合并 dev 分支。

执行 git log --all --graph --oneline --decorate 查看拓扑结构如下(合并后):

除了知道这两种合并方式外,我们还要知道在合并过程中的冲突原因以及处理策略。

原因:当两个分支修改了同一文件的相同部分时,Git 无法自动合并,此时便会产生合并冲突。

解决步骤:

  1. 运行 git merge 后遇到冲突
  2. 使用 git status 查看冲突文件
  3. 手动编辑冲突文件(文件中有 <<<<<<<, ======= 和 >>>>>>> 标记),删除冲突标记并保留想要的代码
  4. 使用 git add <文件名> 标记冲突已解决
  5. 继续合并:git merge --continue
  6. 完成合并:git commit

如果不想合并了或者冲突无法解决,执行 git merge --abort 回到合并前的状态。

此外 git merge 命令本身还提供了一些常见的合并选项以及合并策略,我们这里简单介绍一下。

高级合并选项

使用方式为 git merge [option] branch_name 如 git merge --ff-only dev。

  • --no-ff 强制创建新的合并提交,即使合并可以通过快进合并完成
  • --ff-only 仅允许快进合并,若无法快进则合并失败
  • --squash 将待合并分支的多个提交压缩为单个新提交,不保留待合并分支的提交历史
  • --no-commit 合并代码到目标分支的工作区但不提交,允许手动检查或调整

合并策略参数

使用 -s 指定合并策略,不加 -s 参数时默认为 recursive 策略。大多数情况下用默认策略就足够了,使用方式如 git merge -s recursive dev。

常用合并策略有

  • recursive 基于递归三路合并算法,通过共同祖先、目标分支和待合并分支的提交进行三方合并,自动解决非冲突修改,冲突部分需手动处理
  • resolve 简化版的三路合并,仅寻找一个共同祖先,避免递归合并虚拟节点,可能减少自动合并但增加冲突概率
  • octopus 同时合并多个分支,如 git merge -s octopus feature1 feature2 feature3
  • subtree 将子目录作为独立仓库合并,适用于模块化项目或子仓库管理,如 git merge -s subtree module-folder
  • ours 完全保留当前分支内容,忽略待合并分支的所有修改(即使无冲突),仅记录待合并分支的提交历史

冲突解决策略

使用 -X 参数指定冲突解决策略,不加 -X 参数时默认不会应用任何冲突解决策略。使用方式如 git merge -X ours。

常用的冲突解决策略有

  • ours 冲突时自动选择当前分支的代码,非冲突部分正常合并
  • theirs 冲突时自动选择待合并分支的代码,非冲突部分正常合并
  • ignore-space-change 忽略空格差异(如缩进或换行符),减少无意义冲突
  • patience 优化差异比对算法,更关注代码结构而非逐行匹配,提高合并准确性

综上,我们可以写出如下命令

1
git merge --no-ff -s recursive -X patience dev -m "Merge dev"

Git Rebase

Rebase(变基)本质上是将一系列提交从一个分支移动到另一个分支的顶端。注意:在变基操作中,"当前分支"通常为个人开发分支,如 dev 分支。细品接下来的 5 个步骤。

Git Rebase 都做了哪些事?

  1. Git 会先找到当前分支和待合并分支(如 main)的共同祖先提交,即分叉点
  2. 然后提取当前分支上所有分叉点之后的提交记录(保存为补丁)
  3. 丢弃当前分支分叉点之后的提交记录(第 2 步的补丁还在)
  4. 在当前分支的顶端应用待合并分支分叉点之后的提交记录
  5. 在当前分支的顶端根据第 2 步的补丁创建一组全新的提交

还没明白?我们举个例子,场景如下:Dabby 从 main 分支的提交 Initial commit 拉取了 dev 分支,然后进行了提交 C 和 D。与此同时,main 分支也创建了一个 A 提交。

最后在 dev 分支上执行 git rebase main 变基 main 分支。

因为是纯线性历史,这里就不放拓扑图了。以下是提交记录的线性关系表示。

变基前

main: Initial commit -> A

dev: Initial commit -> C -> D

变基后

main: Initial commit -> A

dev: Initial commit -> A -> C' -> D'

可以看到,1.变基后并不会修改待合并分支(main)。 2.变基后自身(dev)的提交历史发生改变。3.如果想让修改合并到 main 分支则必须切换到 main 分支后执行一次 Fast-forward 合并。

变基遇到文件冲突后我们可以解决:

  1. 运行 git rebase main 后遇到冲突
  2. 使用 git status 查看冲突文件
  3. 手动编辑冲突文件(文件中有 <<<<<<<, ======= 和 >>>>>>> 标记),删除冲突标记并保留想要的代码
  4. 使用 git add <文件名> 标记冲突已解决
  5. 执行 git rebase --continue 继续变基

或者执行 git rebase --abort 取消变基。

有个有意思的事,git rebase 命令还有一个 -i/--interactive 参数,可以开启交互式变基模式,允许用户编辑提交历史(如合并提交、修改提交信息、调整顺序等)。

使用场景

通过上面的学习,我们对 Git Merge 和 Git Rebase 的基础概念以及合并方式有了一个大致的了解,接下来我们需要了解他们的区别以及在各种场景下应该使用什么方式。

一张表格总结它们的核心区别:

综上所述:

  • 一般无特殊需求的情况下都用 git merge 是最稳妥的选择。
  • 多人协作下的公共分支(如 main 分支)必须用 git merge。千万不要在公共分支上执行变基操作。
  • 个人项目或者本地项目想用啥用啥。

优雅,实在是优雅。