前言
在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是个强大的命令,它能够帮助我们保持分支的简洁以及整合或修改历史提交。但是在我们将提交已经推送远程的时候,还是尽量别去使用以免导致和其他同事出现协同问题。