Git: merge vs rebase

Merge 还是 Rebase,这是一个问题。网上有许多教程说明二者的区别,之前我写的一个 关于 Git 的 PPT
里也说过两者的区别。这篇文章里,我们从分支图的角度,看看两种策略下产生的分支图有什么区别。

理想的分支图

这里我们要说明的是, Git 是用来解决多人协作的代码管理,尽管也可以“单机”使用,但它的一些优势或是缺点要在多人使用时才会显露出来。

现在假设我们独自开发一个产品,一个个往上加功能,那么最终的 git 分支图会像这样:

│
M─┐ Merge branch 'feature-2'
│ o [feature-2] commit #3
│ o [feature-2] commit #2
│ o [feature-2] commit #1
M─┤ Merge branch 'feature-1'
│ o [feature-1] commit #4
│ o [feature-1] commit #3
│ o [feature-1] commit #2
│ o [feature-1] commit #1
I─┘ Initial commit

这里采用的是 tig
的分支图符号。可以看到的是每个功能都用了几个 commit,开发后合并到 master
分支中,再基于最新的代码继续开发下一个功能。清晰明白。

但如果多个人一起开发,或者有多个并行开发的功能,那么事情就开始变复杂了。

Merge

现在我们考虑多人开发多个 feature,那么最幸运的情况是像这样的:

M─────┐ Merge branch 'feature-2'
│     o [Feature-2] commit 2
│     o [Feature-2] commit 1
M───┐ │ Merge branch 'feature-2'
│   o │ [Feature-2] commit 2
│   o │ [Feature-2] commit 1
M─┐ │ │ Merge branch 'feature-1'
│ o │ │ [Feature-1] commit 2
│ o │ │ [Feature-1] commit 1
o─┴─┴─┘ base commit for all features

可以看到有多列,代表不同的 feature
分支,它们最终都合到 master
里。这就是采用 merge 策略最常见的分支结构。一般同时开发的分支越多,列数越多。

上面这个图像是理想的并行版本,它要求所有 feature
基于同一个 commit,且每个 commit 的时间是线性的,所以现实中基本不可能满足这种情况。

如果每个 commit 的时间不同,分支的线就会开始交叉:

M─┐     Merge feature-1
M─│───┐ Merge feature-3
│ o   │ [1] Commit 2
│ │   o [3] Commit 2
M─│─┐ │ Merge feature-2
│ │ │ o [3] commit 1
│ │ o │ [2] commit 2
│ │ o │ [2] commit 1
│ o │ │ [1] commit 1
o─┴─┴─┘ base commit for all features

上面这个图追踪起来就比较麻烦了,如果考虑到 commit 数量多的话,情况就更糟糕了。我们这里所有子分支是从同一个 base commit 开始的,如果各个分支的起始 commit 不同,分支图就会变得特别乱了。

M─┐     merge feature-1
│ o     [1] commit 2
M─│───┐ merge feature-3
│ │   o [3] commit 2
M─│─┐ │ merge feature-2
│ │ o │ [2] commit 2
│ │ │ o [3] commit 1
│ │ o │ [2] commit 1
o─│─│─┘ base commit 2
│ o │   [1] commit 1
o─┴─┘   base commit 1

上面的救命图可能还相对容易看懂,这是因此 commit 数量少,分支数也少,另外各个 feature 分支上都没有 merge commit,否则会更复杂。

综上,在分支图上,merge 会导致分支图的列增多,且依据分支的初始 commit 不同及 commit 的时间不同,会使分支图有更多的交叉,导致历史难以追踪。

rebase

其实大家使用 rebase 的一个重要特点是 rebase 能产生线性的分支历史。考虑这样一个分支图:

(master)
│ (feature-1)
o │ c
│ o b
│ o a
o─┘

如果我们此时在 master 分支执行 git merge feature-1
,则和之前 merge 一样,结果会变成:

M─┐
o │ c
│ o b
│ o a
o─┘

但如果我们在 feature-1 上执行 git rebase master
,则会产生下面的图形。注意的是 rebase 是会产生新的 commit 的, a
变成了 a'
,如果用 git show
看其中的内容,可以发现虽然 commit message 相同,但 diff 已经是不同了。

(master)
│ (feature-1)
│ o b'
│ o a'
o─┘ c
o

可以看到,分支 feature-1
的初始 commit 变成了 c
。这时候取决于 merge 的方式,会有不同的效果。一是在 master
上执行 git merge feature-1
,这时 git 会判断可以 fast-forward;二是通过 gitlab 或 github 等提交 Merge/Pull request,它们依旧会创建一个 Merge commit,如下:

local merge                 gitlab/github
(master, feature-1)         (master)
│                           M─┐ m
o b'                        │ o b'
o a'                        │ o a'
o c                         o─┘ c
o                           o

但注意到即使 gitlab 会创建新的 merge commit m
,在 master 的 c
m
之间也不会有任何“插队”的其它 commit。突出一个清晰明了。

这时考虑多个 feature 同时开发,大家在合并前都先 rebase 最新的代码,就能做成“线性”的图形:

(master)           (master)           (master)              (master)
│ (feature-1)      │   (feature-2)    │ (feature-2)         │
│ │ (feature-2)    │   │              │ │                   M─┐ merge feature 2
│ │ │              │   o              │ o                   │ o
│ │ o   ====>      │   o    ====>     │ o        ====>      │ o
│ │ o              M─┐ │              M─┤                   M─┤ merge feature 1
│ o │              │ o │              │ o                   │ o
│ o │              │ o │              │ o                   │ o
o─┴─┘              o─┴─┘              o─┘                   o─┘

   (merge feature-1)   (rebase master)   (merge feature-2)

但在实际的团队开发中,要达到上面的要求需要“串行”提交代码,即上一个人的代码合并之后,下一个人再 rebase 最新代码并提交新的 Merge/Pull request。这是不现实的。经常的情况是所有人都在 deadline 临近时一起提交,是一个“并行”提交的过程。并且现在大家一般在 merge 前都会有一些 CI 的检查,如果串行,这些检查也得串行地执行,太耗时间了。也因此, rebase 比较合适使用在“内部”分支上。例如一个 feature 有多个 task,那么 task 分支合并到 feature 分支时,使用 rebase 比较合适。

一些情形下,我们会发现有一些 commit 出现了多次,假设现在我们开发一个 feature,包含两个子任务,这两个子任务是在 feature 开发了一定时间后开始的,于是出现这样的分支图:

(master)
│ (feature)
│ │ (task-1)
│ │ │ (task-2)
o │ │ │ c
│ │ │ o t2
│ │ o │ t1
│ o─┴─┘ b
│ o     a
o─┘     base commit

现在,两个 task 前后完成了开发,于是向 master 发起了 Merge/Pull request。在发之前,先 rebase 了 master
,于是产生了如下的分支图:

(master)
│ (feature)
│ │ (task-1)
│ │ │ (task-2)
│ │ │ │
│ │ │ o t2
│ │ │ o b"
│ │ │ o a"
│ │ o │ t1
│ │ o │ b'
│ │ o │ a'
│ o │ │ b
│ o │ │ a
o─┴─┴─┘ c
o       base commit

可以看到,rebase 过后, task-1
task-2
分别生成了自己对应的 commit a
b
的复本。那么当 task-1
task-2
独立被合并到 master 时,这些复本都会被保留:

(master)
M───┐
│   o t2
│   o b"
│   o a"
M─┐ │
│ o │ t1
│ o │ b'
│ o │ a'
o─┴─┘ c
o     base commit

所以,此时如果在 master
分支上用 git log
查看历史,会看到有两个 a( a'
a"
) 两个 b( b'
b"
)。这是用 rebase 容易产生的问题之一,其它需要注意的这里就不深入了。

综上,从分支图的角度上,使用 rebase 能使分支图更“直”,但如果使用不当,也容易出现一个 commit 被提交了多次的情况。

一些建议

结合上面我们看到的情况,管理分支时,我的建议是“从哪来,回哪去”。

例如一个 task
分支是从 feature
分支出来的,那么最好合并回 feature
分支,而不要直接合并到其它的分支(如 master
)。这样能防止 commit 被提交多份。在“回哪去”的过程中,尽量使用 rebase
,最大程度保证分支图的“线性”结构。

当然最重要的还是明白背后的原理,这样才能灵活使用。

稿源:三点水 (源链) | 关于 | 阅读提示

本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 综合技术 » Git: merge vs rebase

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录