温故知新之git rebase的用法

4,135 阅读6分钟

Git作为目前程序界中使用最为广泛的版本控制系统,熟练使用Git工具是“优秀代码搬运工”的重要表现。 在多人协作的项目中,分支合并是经常需要处理的事务,而rebasemerge是合并的两大工具。即便是工作甚久,我对rebase命令也只是略懂皮毛,本文总结了rebase命令的几个常见用法。

我们不生产commit,我们只是commit的搬运工

Rebase

git - rebase来自官方的解释是:Reapply commits on top of another base tip。中文两字就能概括:变基。使用生活中的例子,就像接线头一样,将一段线条接在了另一个线条后面,最后它们依然是一条线。

接下来,我们将从不同应用场景去分析如何“接线头(变基)”。

1. 更新本地分支

假如你在本地master分支中已经开发了部分功能,产生了一些提交。与此同时,你的小伙伴也努力工作,并将他的劳动成果推送到了远端分支origin/master上。此时,你需要将远端分支更新到本地,之后你才可以继续推送(push),分支情况如下图所示:

通常,我们的做法是执行git pull命令,该命令会自动下载分支并完成合并。但git pull命令默认采用的合并策略是merge方法,即git pull是如下两条命令的缩写:

git fetch origin 
git merge origin master

最终它会形成如下分支图:

合并远端分支,你将在本地获得一个崭新的提交H,主要内容是远端分支所有提交内容与你本地分支提交内容的混合大杂烩。 显然,你并不想每次拉取远端内容时都会产生一条“丑陋”的旁路分支,长此以往,你的Git Graph将变的和地铁线路一般的错综复杂。于是,你仅想要“接线头”,将本地开发分支所有提交直接连接在远端分支的提交后面。rebase出马,一马平川。

git pull --rebase
// 等价于
git fetch origin
git rebase origin master

运行上述命令后,我们得到如下分支图:

直观上看,rebase会粗暴的将C --> F的连接掐断,直接连接在提交E的后面,图中提交F'G'的内容与原提交FG内容是一致的,但commit id却完全不同。即“变基”操作会为原提交分配新的commit id,并将新提交连接在新的变基点上。

当然在rebase过程中并非一帆风顺,rebase会检查新提交F'是否与目前分支上的内容产生冲突,如果出现了冲突,git会暂停变基过程,等待你解决完所有冲突后再继续:

// solve your conflicts, then
git add .
git rebase --continue
// or
git rebase --abort

虽然提交内容没有变化,但对于Git而言,变基后的提交会被视为完全不同的提交。

2. 合并commit

阳光明媚的某一天,你在本地开发了一个逻辑复杂的功能,产生了多条commit记录,而commit message是随手填写的,毫无意义:

* 50827 - fix      // E
* 312ed - a bug    // D
* bdc5c - done     // C
* 7a169 - hahaha   // B
* 8rf4q - Empty commit to fix CI on master  // A

这个逻辑复杂的功能经本地测试后完美运行,是时候推送到远端让其他小伙伴们膜拜你的代码了。但是你的毫无意义的提交信息却没有体现出你到底做了些什么。需要对这些“无聊”的提交修剪修剪了。

我们的目标是将 B --> C --> D --> E 整合为一个完整提交B',并指定新的commit message:

运行如下命令:

git rebase -i [start] [end]

-i会唤起交互式界面让用户编辑以完成变基操作,其中[start][end]分别对应了需要操作的commit id区间(左开右闭),如果省略了[end],则该区间的终点默认是当前分支HEAD所指向的commit,在本例中分别为8rf4q50827

git rebase -i 8rf4q
// or
git rebase -i HEAD~4

我们进入到交互界面(即vim环境):

在界面上方列出了需要编辑的所有提交,在每个commit id前的是指令类型(pick),在Commands中有相关的指令说明。

p, pick = 保留该commit
r, reword = 保留该commit,但修改它的提交信息
e, edit = 保留该commit,在合并该请求时暂停
s, squash = 保留该commit,合并到前一个提交中
f, fixup = 类似于squash,但抛弃提交它的提交信息
x, exec = 执行shell命令
d, drop = 丢弃该commit

在本例中,我们需要保留提交B,将提交C D E均合并至上一个提交中即可:

完成编辑后保存退出(esc + shift : + wq)即可。随即,进入到修改提交信息的交互界面:

将不需要的message注释掉(添加#),或者修改提交信息即可。我们给这次变基记录提供一个漂亮的提交信息:

经过以上步骤后,你就获得了一个整合多条commit的新的commit - B': This is a very nice commit message

合并多个commit在开源社区提起Pull Request(PR)时非常重要。在正式提交PR前需要合并你的提交,整合为一条或若干条有意义的提交,让你的工作有迹可循,不要将编辑提交历史的责任扔给仓库维护者。

3. 删除commit

删除commitrebase中另一个有趣的用法,考虑如下分支场景:

提交BC是需要删除的提交,可以运行如下命令:

git rebase --onto dev~5 dev~3 dev 
// or
git rebase --onto HEAD~5 HEAD~3 HEAD 

HEAD指向当前版本,即dev分支的F提交所在处;
HEAD^指向上一版本,即E提交所在处;
HEAD~3HEAD^^^的简写,依次类推;

依据左开右闭的规则,git rebase --onto HEAD~5 HEAD~3 HEAD的语义是将D --> F提交区间连接到提交A之后,即变基,以此达到删除的目的。

rebase命令的风险

变基过程可以让你以线性方式来管理你的提交,但rebase命令会修改你的提交id,说白了,rebase改变了历史,改变了过去,“严谨的历史学家”可不能容忍随意的更改历史呀,所以它很危险。

reabse命令魅力无穷,使用得当它会使你的提交脉络清晰明了,使用不当你会陷入反复解决冲突的漩涡里。为避免错误使用rebase命令,在这里引出一个使用rebase命令的“金科玉律”:

永远不要去rebase本地之外的任何提交

如果发现你操作的那些commit已经被推送到远端或者被其他小伙伴合并到本地,请停下变基的操作。 只要遵守这条定律,使用rebase命令修改私有分支的历史记录就不会出差错。

你的小伙伴可不想你随意篡改他的劳动成果哦!

参考

最后

码字不易,如果:

  • 这篇文章对你有用,请不要吝啬你的小手为我点赞;
  • 有不懂或者不正确的地方,请评论,我会积极回复或勘误;
  • 期望与我一同持续学习前端技术知识,请关注我吧;
  • 转载请注明出处;

您的支持与关注,是我持续创作的最大动力!