「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战」
前言
在Git中整合不同分支的修改主要有两种方法:merge和rebase。其中merge在一般的团队中使用的比较多,而rebase则使用的比较少。本篇文章将主要介绍变基(rebase)的概念以及探讨我们应该在什么时候使用它。
什么是变基
变基就是提取当前分支的commit一步步应用到基底分支形成新的commit。
git rebase master
变基的原理如下
-
寻找变基分支与基底分支的最近公共祖先节点
-
提取当前分支相对于该祖先的历次commit修改
-
将历次修改应用到基底分支(新的commit 原先的commit-msg)
-
如果基底分支和当前分支相对于祖先节点同时修改某处将产生冲突,需要手动解决冲突执行continue
对于变基实质也是对
提取的commit
和基底分支的最新commit
及最近祖先commit
进行Three Way Merge
,这点和merge过程的三方合并是一致的。大家可参考上篇文章进行阅读深入Git-分支及合并
为什么使用变基
在大家已经理解变基的基础上,我们再来分析下为什么使用变基?
通常在我们使用merge的时候,经过不断的分支创建合并将自动生成很多merge相关的commit,同时分支图谱也会变的非常错综复杂。
那么有什么办法在合并的时候可以不用生成新的commit并且让分支图谱看起来简洁呢?那答案就是变基。
场景1
团队中多人在同一功能分支开发,当远程分支有更新的时候我们一般需要先执行pull命令拉取最新提交,而pull命令实际由两个命令组成
git fetch # 获取远程所有分支更新
git merge origin/feature-xxx # 合并远程分支
我们常常会忽略拉取代码时候的合并,直到我们遇到冲突才会留意。实际每次我们去拉代码都有可能因为合并而产生新的merge commit
。这时候我们可以通过变基来消除本次多余的commit。
git fetch
git rebase origin/feature-xxx
由前所述,此时将当前分支的所有提交提取应用于远程分支。变基之后,我们再去push就可以直接推送成功,也不会产生多余的合并commit
。
场景2
在开发中,我们可能在不同的功能分支进行开发,但是最终需要合并到master分支进行部署。
我们此时直接执行merge操作,将很有可能产生合并commit
git mrge feature-xxx
但如果我们先执行变基再去merge
git rebase master # feature-xxx分支
git merge feature-xxx # master分支
此时再进行merge就会应用默认策略fast-foward
,不会产生多余的合并记录。
在这里我的理解是应当将变基应用于合并到master分支这步,保证master主线的简洁而非每次合并到dev或者test分支都进行变基。
什么时候不应该使用变基
实际上进行rebase是一种修改操作记录的行为,为了使提交记录变的简洁,我们修改了实际操作记录。
-
原先是在
commit1
节点切出的新分支,我们将其变基,会让其看起来是从commit2
切出的节点。 -
原先是先进行开发再进行合并,变基后让人看起来像是一切就绪后才进行的开发,而且相关的记录顺序也会有所修改。
所以一直有两种不同的声音
-
应该使用rebase来使提交日志变的简洁
-
不应该使用rebase,因为rebase会修改提交日志,而我们应该使用日志记录我们每一步操作。并且rebase使用不像merge那么简单,有可能造成不可逆的结果。
当然,我们在这不去讨论两种声音应该支持哪种,而是指出在什么时候我们的确不应该再去进行rebase
当我们将当前提交推送到远程的时候,此时不应该对当前提交再进行rebase
为什么呢?我们通过案例来理解
假定有A和B两个开发者,A在feature进行了提交D并且推送到了远程,B也拉取了最新代码
这是A对feature进行以dev为基底的变基,此时提交D变修改为提交D2
此时用户B再去拉去远程分支,就会出现merge,并且提交记录将包括A提交的两次D(D和D2),尽管D和D2实际是同一份修改。以后在执行log命令查看日志的时候就会出现两个D提交,让人看起来有点莫名其妙
rebase参数
onto
在前面分析rebase的时候,我们说过会提取commit的差异进行重新生成commit。
所以假定我们在A分支切出B分支,在B分支再切出C分支。此时我们在C分支执行rebase的时候会将B分支的内容一起提交到A分支。那么如何通过变基将C分支的提交内容(EF)单独变基到A分支呢?
git rebase --onto A B C
通过onto命令就可以摘出(B, C]的提交单独变基到A分支。
实际上上面的BC不一定是分支,也可以是个commitID,即我们可以通过onto命令将区间(B, C]的提交变基到A分支,注意区间开闭性。
i
我们再来了解一个有趣且强大的rebase参数i。
git rebase -i commitxxx
他将允许我们对commitxxx后(不包含ommitxxx)的所有提交进行修改
pick 76e0513 master2
pick 3f7e15b master3
pick 77737ed dev1
pick a2d0cc4 dev2
pick 258ce19 dev3
# Rebase 7ab7f31..258ce19 onto 7ab7f31 (5 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
我们可以将上面的pick修改成其它,上面的注释实际已经说明可选的选项。我们介绍下常用的几种
-
pick 不作任何修改
-
reword 修改commitMsg
-
edit 修改commit内容
-
squash 将commit合并到上次commit
-
fixup 和squash类似,但是不会保留commitMsg
-
drop 移除commit
结语
rebase是个强大的命令,它能够帮助我们保持分支的简洁以及整合或修改历史提交。但是在我们将提交已经推送远程的时候,还是尽量别去使用以免导致和其他同事出现协同问题。