git rebase的使用场景

本来打算写一篇关于自己投资知识水平的文章,其实都差不多快写好了,但最近这几天每天连着亏,感觉文章内容又能扩充了,回头再发吧。今天这篇文章是关于我最近对git rebase命令的使用心得。适合掌握git基础使用,没怎么用过git rebase的人阅读。

对于git的初学者来说,能使用

  • git add
  • git commit
  • git push

就能进行最基本的使用了。

接下来的话,还需要掌握

  • git branch

  • git checkout

  • git pull/fetch

  • git merge

等命令的使用,也就是对git的分支工作模式有所了解。在不太频繁的情况下,还需要用到git loggit reset等命令用于版本回退。

如果熟悉了上述命令,应该就可以使用git和他人协同进行开发工作了。那么git rebase究竟是做什么用的呢?

首先,从实践经验来看,即便完全不使用git rebase命令,一般也不耽误和别人一起协同开发。我参加开发工作的头一年基本没主动用过rebase命令。但最近我从实际工作中对rebase命令的使用来看,我认为虽然rebase命令使用的场景有多个,但使用这个命令的目的都是一致的:为了更清晰整洁的git提交记录。

在说具体场景前,我先说一下我对这个“rebase”这个名字的理解。这个名字说实话有一定迷惑性,因为他的使用场景确实比较多,但我觉得可以这样理解:re-base,即变换当前分支的base。比如我当前在dev分支上,执行git rebase master,作用就是把dev分支的”base“分支(不管dev之前的base分支是谁)变为master分支。这应该就是rebase命令最原本的含义。网上有大量图文教程描述这个过程。

那么rebase的实际工作场景是什么?这是很多教程没有提到的,我遇到过如下场景:

1. 主干分支的代码更新了

这应该是最常见的一种场景,而且只会在多人协同开发时遇到。

比如,我从master分支checkout出dev分支进行开发,但在我开发dev的同时,master分支的代码已经更新了(比如,同事把bugfix分支合并到了master上)。此时,可能存在如下操作:

  1. 【不正确的操作】本地的master没更新(git pull),我在本地把dev合并到了master上,此时不会产生任何冲突,合并后尝试git push master。此时,会push失败,如果尝试pull,有可能产生代码冲突,如果我在本地尝试解决代码冲突,我有可能覆盖我同事的代码
  2. 【不正确的操作】首先更新本地master分支,然后尝试将dev合并到master分支上,有可能产生代码冲突,和上个例子一样,可能覆盖同事的代码变动。

不过上述两个例子,都需要在本地执行git push master,那么对于一个权限配置正常的、使用master-dev分支模型进行开发的Github、Gitlab仓库来说,是不应该开放git push master这样的权限的,因为这样的话显然对master分支来说比较危险。我曾经有过一次疏忽,以为自己是在feature分支上开发功能,但其实是在master分支上。我一路git add, commit, push,没有感到任何异常,但是其实是推到master上了。一个好的防呆设计,不应该依赖人的可靠性,而是在人变得不可靠的时候,避免因为不可靠带来的潜在损失。

当然还有一个前提,就是对于主干分支代码更新的前提下,什么情况下会出现上述两种操作方式呢?如果仓库是运行在github或gitlab上,代码合并走的是PR或MR模式,那就应该不会出现上面的情况。因为,如果进行PR或MR时,代码并没有产生冲突的话,是不用在意主干分支发生了更新这件事的。所以,当PR或MR时,发现系统提示存在合并冲突时,才需要考虑进行下面的操作。

  1. 【正确的操作】我应该先更新本地master代码,并尝试将master往dev分支上进行一次合并。具体的操作可以是:git checkout dev, git merge master。这样的话,就会把master分支上后来出现的那些修改,合并到dev分支上,如果有冲突就处理冲突。但这样的缺点是,会在git历史记录上行程一个master到dev的“反向合并”,会让分支记录显得杂乱。
  2. 【也许是更好的操作】先更新本地master代码,用rebase替换merge,也就是git checkout dev, git rebase master。也就是,将dev分支的“base”变为最新的master代码,有冲突解决冲突即可(但是,如果dev上有多个commit,master的后来改动会被记录到哪个commit上,我还需要确认)。使用rebas这个方式,可以避免git历史记录里的反向合并。从最终的代码合并结果上来看,和merge命令是等价的。

2. 整理git历史记录

这个场景只适用于一个人的git仓库,或者,多人协作情况下自己开发的分支。

比如,我在dev-wangyufeng分支上开发了一个很大的功能,前前后后一共有5个commit,但其实这5个commit都完成的是一个完整的功能,其中有些commit是进行了bug的修复、添加了代码注释,诸如此类。如果我把dev-wangyufeng直接合并到master并push,就会在git历史记录里把我的这几个啰嗦的commit都添加进去。

如果我想在合并分支前,整理好我要提交的commit记录,我会使用git rebase -i,交互式地整理我的开发分支上的commit记录。我可以进行合并、commit message的修改,commit顺序的变化等。

我使用git rebase -i的方法一般是,后面跟的参数是checkout分支时,当时master分支的那个commit id,比如git rebase -i xxxxxx。另外这个命令有一个挺有意思的地方,它列出的commit列表,从上到下是按从旧到新的顺序排列的,和git log正好相反,刚开始的时候有点令人迷惑,而且在列出的commit里,还不包含参数里指定的xxxxxx这个commit。

如果在整理分支前,手滑给git push了,只要分支还没合并,在做任何修改后,用git push --force-with-lease就可以更新了。使用任何带“force”参数的命令都请小心。

但是公共分支是没办法整理的,或者说整理后的代价很大。这里有点区块链的意思,修改了一个历史commit,这个commit后边的commit全都得重新生成,那这对同事来说可能会比较困扰。

3. 消除分支记录

我听说过这个用法,但是从来没用过,它的意图是让分支记录在git记录里都看不到,只能看到唯一一根master主干分支的记录,把开发分支的commit挪到master上展现。我不这么用是因为一是我不太喜欢这样,二是身边确实也没人和我协同开发时能跟我一起使用这样的方式。


所以我对git rebase使用场景的认识就是以上三点。其实,我也听过一种说法,就是认为应当尽可能少的使用rebase,背后的思想是尽可能少地修改git历史记录,甚至完全不修改。因为有人认为,git的历史记录就应当如实反映一切真实发生过的事情,git历史记录不对人的可读性负责,而是对历史记录负责,对代码版本的完全可追溯性负责。也有人认为,git历史记录越整齐越简洁越好,git历史记录完全是为可读性服务的。

我个人更倾向于前者的观点,也就是git应当尽可能记录真实发生过的历史,但也应当对历史记录的“信息密度”进行适当的控制,那些太没信息量、太冗余的信息就不要污染仓库了,对程序员们的阅读来说也是个负担。

随便扯点发散的想法。关于如实地记录一切真实发生的历史的这个想法,我确实有过一些思索,尤其是在发生http 403, 404的时候,更容易想这些事情。针对某些特定的领域,我是一个技术至上主义的人。比如,对于保护网民传输的数据不受盗用、非法修改和监听,我认为最终可靠的方式是依靠密码学技术,以及建立在密码学技术之上的一套体系,比如RSA、AES,TLS等。而不是靠网站的良心,或是仅仅提高作恶的成本。当然我认为提高作恶的成本是应该的。TLS给人带来的一种平等,一种超越人类社会通常所说平等的“元平等”。就好比监控系统硬盘的损坏概率是一种平等,但新冠病毒对人的感染能力是另一种平等。

区块链的的潜在应用有很多,合约、追溯等等,如果有人问我,你认为区块链还有什么应用场景呢,我认为区块链在技术上来讲,是一个可能的,“如实记录一切历史”的技术方案。这里讲的“如实”不是我们通常语境中的如实,而是…一切主观现实。不是记录唯一的共识,而是记录真实的一切,而真实的一切注定没有客观成分,唯有主观。当记录的足够充分和全面时,也许就能产生某种效果,某种对“唯一结论”产生方式的影响。就好比,对网站访问者流量的保护,从网站的良心和作恶成本,转向密码学技术,让对公平的保护从一种产生模式,转向另一种更高层次的产生模式。在这种情况下,区块链上记录的99.999%…的信息都可能是垃圾信息、冗余信息,信息量趋近于0,但有价值的信息总是能通过某种方式涌现出来…就好比Google的搜索结果里,pagerank算法为我们呈现出了最有价值的信息,不是吗?在现在,我们人人已经拥有的的是“普遍视角”产生的共识,但当普遍视角难以达成时,区块链技术也许是记录诸多“客观视角”的一种手段。对于“普遍视角”,我想说的不是“我们大家”的普遍视角,而是“我”和“他们”的普遍视角。这个普遍视角的达成,真的不容易。