让commits历史像Vue一样清爽优雅-Git rebase 原理、工作流介绍+常见问题指南

2,828 阅读13分钟

1. rebase原理

a. 概述

  1. 用处:rebase能合并提交、净化commit历史;它也能移植分支,把一些分支的提交移植到另一个分支
  2. 最大的优势:commits历史干净清爽,git graph中能尽量呈线性延伸、减少了交叉情况,易于代码的维护和管理

尤大在知乎的回答,多用rebase能够有效的让commits历史变得更为清爽——这一操作被广泛用在开源项目贡献中,同样也可以利用rebase操作让我们的Git工作流变得更为精简

b. rebase和merge的区别

rebase(变基)的作用,就是可以改变一个分支中一串commit的‘基点’、也就是父commit。这样说起来可能并不好理解,所以我们可以将它和开发中常用的merge操作进行类比——它和merge操作的目的是类似的:将对代码的更改,从一个分支集成到另一个分支

因此,不妨假设:git rebase ≈ git merge,把两种命令代入到例子中——一个简易版的、由Master主分支和Feature开发分支组成的日常开发环境。在示例中,用两种命令实现同一工作流,来比较它们的异同、理解rebase的原理。 如上图,假设一个简易的日常工作环境:多人合作开发,在基于commit 2的Feature分支上进行新功能的开发时,Master分支中有一些新的提交,需要在自己的分支上同步Master的更新。

1. 使用merge操作

我们常会使用pull来拉取远端的代码,git pull = git fetch + git merge,也就是说,在使用pull时实际上也是在使用merge操作合并修改。通常来说,我们会使用如下的命令操作:

git checkout feature
//切换活动分支到feature
(feature)git merge master

这会在feature分支中创建一个新的“merge commit”(下图中的commit 8),将两个分支的历史联系在一起,产生如下所示的分支结构: merge操作是很常用的,它会保留原有的分支结构、用一个新的commit来实现对两个分支上代码修改的合并。使用merge,可以避免使用rebase时所有可能的问题。

但另一方面,这意味着每当我们在Feature分支同步Master分支的代码时,都会产生一条实际上没啥意义的、merge操作产生的commit节点(图中的commit 8)。这会产生两个问题:

  1. 如果需要多次拉取Master分支的最新代码,多次合并产生的多次commit节点会污染我们的commit记录,让commit记录变得冗杂、可读性和维护性变差。
  2. merge操作会让两个分支间产生交叉、形成“钻石链”。这会让我们的git graph变得凌乱,不易管理和维护。

2. 使用rebase操作

在同样的场景下,可以使用以下命令,将feature分支rebase到master分支上:

git checkout feature
git rebase master

在使用rebase前,Feature分支的3个commit(3,4,5)是基于commit 2的。通过执行git rebase命令,Feature分支的的基点变成了commit 7.

实际上,rebase的所做的就是为Feature分支上的3个提交(commit 3,4,5)创造了一个副本、将这个副本移动到master分支的顶端,从而有效地整合了所有master分支的新提交。具体来说:Git会让我们想要移动的提交序列(commit 3、4、5)在目标分支(Master)上按照一样的顺序再现一遍,相当于一个【副本】——图中的3'、4'、5',与3、4、5拥有一样的修改、一样的作者、日期、注释信息,不同的是rebase这些副本拥有与原本的提交不同的全新commit id。

与merge操作的效果一样,在完成rebase操作后,Feature分支就成功整合了Master分支的所有更新。

c. 优缺点总结:

Git官网文档对rebase操作的优点的阐述如下:

这两种整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。 你在查看一个经过变基(rebase)的分支的历史记录时会发现,尽管实际的开发工作是并行的, 但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。
用大白话来说,使用rebase的好处主要是能获得更清晰的commit历史记录。它消除了不必要的git merge产生的merge commit;同时,如上图所示,rebase操作能够产生线性的git graph结构。我们可以从feature分支的顶端一路向前追溯,没有任何分叉地追踪到项目的开始——这能让我们更好的读懂项目的修改历史。

但是,rebase操作较merge操作更加复杂、更加危险。它会重写项目的历史记录,这可能对别人的工作造成巨大的影响;同时,merge操作所附加的那一次merge commit能够提示人们何时合并了上游(Master分支)的更改。在使用rebase操作时,我们无法获取这样的提示的。

2. 工作流中的示例

在进入实际操作前,我们首先需要强调一点:所有的rebase操作都会造成对commits历史的修改,所以当我们想把rebase操作后的修改push到远程仓库时,Git会认为我们的操作是“危险操作”,因此我们需要使用 git push --force来强制推送。这就需要我们清楚的知道自己到底在干什么,而这也是笔者写下本篇文章的目的所在:让大家不再害怕rebase,而是在适合应用它的场合适时的使用它,优化我们的Git操作流。

a. 合并不同分支的多次提交:避免多余的merge commit

在与上述类似的工作环境(本地开发后,同步线上master的最新代码,然后并入master)中,分别进行如下操作,在线上观察commit历史日志

//#左图
(feature)开发...
(feature)git pull origin master
git checkout master
(master)git merge feature

//#右图
(feature)开发...
(feature)git rebase master
git checkout master
(master)git merge feature

可以看到,通过rebase操作,我们让commit历史变得更为精简了。同时,在git graph结构中,也保证了直观的线性结构(见后文)

b. 合并同一分支的多次提交:rebase -i 操作

在日常工作中,我们在同一开发分支下开发某一功能时,常会随着需求的小改动、测试的推进,对代码进行不断的修改、微调,然后commit提交、push到线上。过多的commit其实没有意义,我们可以通过rebase --interactive这一操作,对同一分支下的多次提交进行重写,将多次commit合并为一个。

假设我们在分支feature-D上进行了3次提交操作,如下图: 我们可以执行git rebase -i HEAD~num命令,对过去的几次提交做修改(num是可指定的提交的次数),操作示例如下:

//当前所处分支:feature-D
(feature-D)git rebase -i HEAD~3

git会在终端(命令行)生成如下文本,输入vi进入编辑模式即可编辑 在这个列表中需要关注的两个地方:

  • 该列表中最旧的提交在最上面,最新的在最下面,与使用git log查看的顺序是相反的,该顺序是交互式rebase的操作顺序。
  • hash前的单词表示对该提交的操作,该操作有如下几种:
# Commands:
# p, pick = use commit 默认
# r, reword = use commit, but edit the commit message   
  使用该提交,但是可以修改该提交的提交信息,当时用该命令时,接下来就会进入一个页面,
  让我们写新的提交信息。如果我们想修改那个commit的提交信息时,可以使用这个方法
# e, edit = use commit, but stop for amending   
  使变基中断,这时我们可以修改工作区文件,修改完以后git add . => git commit --amend进行重新提交。  
  提交完后使用git rebase --continue继续变基。如果你想修改某个提交,可以使用该方法
# s, squash = use commit, but meld into previous commit   
  合并提交,它会合并到前面的提交中,并且允许我们重新编辑提交信息,这也是我们要实现的。
# f, fixup = like "squash", but discard this commit's log message 
  也是合并提交,但是没有然我们重新编辑提交信息,而是丢弃这些提交的提交信息
# x, exec = run command (the rest of the line) using shell 
  丢弃该提交,也就是该提交不会复制过去。
# d, drop = remove commit

如果想将这三次提交都合并到最旧的提交上,需要把后两次提交的操作修改为squash,如下: 完成上图的修改后,会进入新的文本编辑页面,允许我们为合并后的新commit写新的注释,如下图 由于我们在本地进行了“三合一”操作,也就是用一个全新的commit概括并覆盖掉原有的3个commit,涉及了对commit历史的修改,因此需要使用git push --force 强制推到线上。完成后如下图: 点击进入9f830b9这一commit,如下图,详情中保留了进行rebase -i操作前的3次初始commit的注释信息。 总而言之,我们通过rebase -i操作,让一个新的commit覆盖了原有的3个commit,编辑了新的概述性注释、同时也保留了原有3次commit的注释。

这时,假设我们想要同步线上分支的代码然后合并到master分支中,执行如下操作:

(feature-D)git rebase master
git checkout master
(master)git merge feature-D

同样的工作流,使用了rebase系列操作后,commit历史明显变得简洁了非常多。

c. 复杂项目下简化git graph的效果

假设有如下图的git结构:基于线上分支(master)进行新功能的开发;完成开发后,我们需要同步master线上分支的最新代码,然后将开发分支的内容同步到测试分支中。 分别不使用/使用rebase操作,操作代码、效果如下:

//左侧
(dev-B) git pull origin master
git checkout st
(st) git merge dev-B
//右侧
(dev-B) git rebase master
git checkout st
(st) git merge dev-B

如果在同样的工作流中,更充分的使用rebase命令 比如在合并到st分支前 rebase到st分支后再合并,就能完全避免merge commit信息;再比如,如果在提交前使用rebase -i,就能把多次的commit提交进行归纳、缩减。

3. 常见问题和解决方案

a. rebase会消除提交历史、甚至影响别人——黄金法则

请遵循Git官方文档中提出并强调的黄金法则:永远不要在公用的分支上rebase别的分支。
先来一个反面典型:下面的操作就是错误的

//在master这样的公用分支下rebase别的分支
(master)git rebase feature-xx

那么我们应该怎么做?引用Git官网文档的说法:

如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基

如果你遵循这条金科玉律,就不会出差错。 否则,人民群众会仇恨你,你的朋友和家人也会嘲笑你,唾弃你。总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行变基操作,这样,你才能享受到两种方式带来的便利。 还是用大白话来说,只要把rebase的使用范围限制在自己的开发分支内,就不会出现影响到他人的情况;在遵循“黄金法则”的前提下,rebase才能成为我们对commit历史进行优化、精简commit提交的利器。

b. rebase会引发更多冲突,怎么避免?

  1. 问题概述:除了可能覆盖commit历史、需要push --force强制覆盖之外,rebase操作还有一个广为流传的“诟病”,即使用rebase时会比常规操作遇到更多的冲突。
  2. 形成原因:在【本地开发分支 feature】进行了多次commit,当rebase到【需要合入的分支master】时,如果feature分支在开发中和master分支有冲突,那么feature的每一个commit都需要手动解决一次冲突。
  3. 问题的图文示例、解决方案:blog.csdn.net/weixin_4405… -i 操作可以有效解决这一问题,详见链接

4. 在实际工作中应用rebase

在写这篇文章时,我也刚刚在团队leader指导下完成了对rebase操作的调研和文档输出、正在推进其在团队内的应用,下面的3点是我认为能够较好的使用到rebase操作优点的场景;在我的设想中,如果充分理解了rebase,他们能够较为快速的接入到现有的工作流中。
当然,可以预见的是,实际的工作过程远比想象的复杂、有更多正在协作开发的仓库和分支,因此在推进rebase操作的过程中也一定会遇到很多问题,我希望能够通过输出这篇文章记录我此时此刻的一些初步粗浅的想法、起到一点点点抛砖引玉的作用,也很希望大佬们指正我的错误、给出一些真正易于应用的范例。

a. 开发过程中:用rebase减少merge commit

(本地开发分支) 在每次开始新一天的开发前,通常都会pull远端的代码,这个使用可以使用rebase(或者 pull --rebase)来操作。这样操作的好处是可以让git graph更清晰、同时减少多余的merge commit记录

b. 合入代码前

先将本地的开发分支 (如feature)rebase变基到想要合入的分支上 (如master),进行完这一步rebase操作之后,再切换到master分支上进行merge feature的操作。
Git的官方文档中的示例所表达的就是这个意思,这样做的好处也是精简commits记录和git graph。

c. 后期持续修改时

在本地多次修复一些小bug、提交到远程仓库前可以在本地开发分支下,使用rebase -i 来缩减提交记录,避免过于冗余的历史出现:

    1. commit 1:完成'xxx'功能;
    2. commit 2:修复‘xxx’功能中的bug1; 
    3. commit 3:修复‘xxx’功能的bug2; 
    4. commit 4:补充‘xxx’功能中遗漏的'yyy'功能;

这一整串的commit 可以被合并成一个总的commit:完成‘xxx’功能的开发和bug修复,然后再合并到远程线上分支中,避免了多次不必要的commit记录。

5. 参考

  1. 一些基础操作及团队协作的介绍 juejin.cn/post/684490…
  2. Git官方文档-rebase(变基) git-scm.com/book/zh/v2/…
  3. 比较清晰的叙述,同时介绍了rebase使用铁律: yrq110.me/post/tool/g…
  4. 掘金上对实操讲的比较好的一篇文章 juejin.cn/post/684490…