guides/git/rebase.md
2021-08-10 17:47:34 +08:00

183 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

使用 rebase 打造可读的 git graph
===
原文https://juejin.cn/post/6844903818136666125
## git graph 可读指什么?
这里的可读,主要指的是能够通过看 git graph 了解每一次版本更迭,每一次 hotfix 的修改记录。反映到分支上面,有两个要求:
* 每个分支的历史修改可读 (单个分支的层面)
* 每个分支的分叉合并可读 (多个分支的层面)
## rebase 是什么,它是更优雅的 merge 吗?
rebase 翻译做 `变(re)基(base)`
讲 rebase 的文章经常会引用三张图:
![](rebase/1.jpeg)
原本的两个分支
![](rebase/2.png)
通过 merge 的结果
![](rebase/3.png)
通过 rebase 的结果
用来说明 git rebase 和 git merge 的区别的时候确实是足够了,但是 git rabase 的用途并非是合并分支,它与 merge 根本不是同样的性质。(注意,这里的说法是`并非是`,不是 `并非只是`,因为虽然有时 rebase 替代了 merge 的工作,但其原理和性质完全不一样。)
rebase 还有以下几种用处:
* `git pull —-rebase` 处理同一分支上的冲突 (如果你能理解其实这是 `git fetch && git rebase` 两个操作,并且理解远程分支和本地分支的区分的话,那么其实他跟单纯的 rebase 用法没什么区别,但是因为其场景不一样,所以单独拆分出来讲)
* `git rebase -i` 修改 commit 记录
实质上:
* merge 是对目前分叉的两条分支的合并
* rebase 是对 `当前分支` 记录基于任何 `commit节点` (不限于当前分支上的节点) 的变更。
rebase的 `base` 不能理解为分叉的基点,而是整个 git 库中存在的所有 commit 节点:
*`git pull —-rebase` 的时候,这个 `当前分支` 是本地分支,`commit节点` 是远程分支的 head
*`git rebase master` 的时候,这个 `当前分支` 是 feature 分支,`commit` 节点是 master 分支的 head
*`git rebase -i` 的时候,这个 `当前分支` 就是当前工作分支,`commit节点` 是在 `-i` 后注明的 commit
## rebase 是怎么工作的?
上面我们已经说到了:
> `rebase` 是对 `当前分支` 记录基于任何 `commit节点` (不限于当前分支上的节点) 的变更。
怎么做到呢?我没有深入研究它真的是如何实现的,以下步骤一定是不对的,但足够让你理解 rebase 干了什么。
我们标注出了两个重点,`当前分支` 和 `commit节点`
*`当前分支` branch-A 从头到尾列出来,从数据结构的角度来说这是一个链表;
*`commit节点` 所在的分支 branch-B 从头到尾列出来,同样是一个链表;
* 找到这两个链表最近相同的节点 n
* 把 A 在 n 之后的所有节点拆下来构成 L
* 把 B 在 n 之后的所有节点中存在的 diff 信息都汇总起来构成 d
* 对于 L 中的每一个节点,把他的 diff 信息拿出来,看看 d 中有没有冲突,如果有没法自动处理的冲突抛出错误,等待用户自己处理;
* 可选地,对于 `rebase -i` 来说,还可以一次取多个节点或者按照不同顺序取,你有更大的处理自由;
* 没冲突和处理完冲突的节点,改一个 hash 放到 branch-B 的 `commit节点` 之后。
你可以把之前我们说到的三种 rebase 用处套在以上步骤看看,是否能够理解。
## rebase 很危险对吗?
对,很危险。
不过就像小马过河一样,光听别人说是没用的,我们需要明白为什么有人说危险,有人说不危险。我看到很多文章说 rebase 有问题,但他们的说法其实并不让人信服,很多时候只是他们不会用。
很多人听说过一个 golden rule在文末有链接但是很少有人会明白真正的原因。让我们一层层地剖析
* 其他人 git push 的时候会对比较本地分支和远程分支的区别,把不同的地方推上去;
* 如果远程分支被修改了,那么其他人的本地分支和远程分支就会出现分叉 (另外还可能造成其他人之前已经推送的工作被覆盖)
* 当出现分叉的时候,意味着其他人需要处理冲突,也就是说,你对于远程历史记录的修改使得 `冲突扩散到了其他人身上`
* 所以我们尽量不能修改远程分支,不能 `把别人fetch回去的改掉`,因为他们的工作就是基于 fetch 回去的分支开展的 (往前推进是必须的,其实也修改了远程分支,所以才会 merge 产生冲突,但是这个冲突是无法避免的)
* 针对上面说的这一条git 也做了限制,如果你触犯了上面的原则,会在 push 的时候被阻挡,但是通过加一个 `-f` 可以强推。
实际上不止 rebase 这样,任何修改远程分支历史的操作都会造成冲突,并且这个冲突需要所有人都解决一遍。
但是分析还是太长了,记不住怎么办?
只需要记住强推 `-f`,只要你不使用 `-f`,那么就是安全的。
不过仅是安全,并不能保证优雅,如果要使 git graph 可读,那你还得多想想:
* 怎么让自己的 commit 历史清晰 (每个 commit 反应了一个单位的工作,前后顺序合理)
* 怎么让每次 hotfix 和 feature 所做的工作和顺序清晰
## rebase 如何让 git graph 可读?
我们还是说回之前提到的三个用法:
### git rebase master
在把分支合并回 master 的时候,用 `git rebase master` 代替 `git merge master`。(注意,只在合并之前使用,否则多人协作会遇到冲突)
这样的好处有两个:
* log里不会出现一个 `Merge branch 'master' into hotfix/xxx`的节点;
* master 分支上在这次 merge 之前已经被提交的 `上一次工作` 和这一次工作的顺序更清晰,因为 rebase 会让这次 feature 的分叉节点改到上一次工作后。对于 master 分支来说,我们并不关心 checkout 新的 feature 的顺序,我们 `更关心 merge 新的 feature 的顺序`
![](rebase/4.png)
比如这里,使用 merge master 导致的紫色的分叉在提交之前与 master 多了一次连接,而且主线上在紫色分叉合并之前还经历了一次合并,这个时间顺序并不清晰。
那么在 master 分支上合并也用 rebase 吗?不是。因为我们需要 master 上的分叉让我们更明白 master 上的改变 (所以使用 `-no-ff`)。
实际上,不管你采用任何 git flow 模型,我都建议你对不太重要的分支合并采用 rebase对重要的分支合并采用 merge。这样会让主干的更改更清晰而分支不会扩散得太远。
### git pull —-rebase
多人在同一分支上工作的时候 (包含 master 分支和多人合作的 feature 等分支),在 git pull 的时候会遇到冲突git pull 的默认行为是 `git fetch && git merge`merge 的对象是远程分支和本地分支。
它的好处基本上与上一条无异,还多了一条:
* 使用 merge 行为的 pull 会将其他人的工作作为外来的分叉,从而在 graph 上产生一个新的分叉, 并且其他人这一段时间所做的所有的工作都会在 graph 上被抬升出去如果这段时间其他人做的工作很多graph 的主线会变得丧失了主线的意义 (因为它太单薄了,很多工作根本没反应上来)。
![](rebase/5.png)
比如这里,本来左数第二条玫红色的才是主线,因为不规范地在 master 上直接提交了一次 commit 并且采用 merge 方式的 pull 做了合并导致主线被抬升到了外层。而这次不规范的 commit 却成了主线。
### git rebase -i
使用这条命令可以修改分支的记录,比如觉得之前的 commit 修改内容不够单元化,像是 `修改了文案1为文案2``修改了文案2为文案3`,这种记录对于 master 分支来说是没必要关注的信息,最好通过 `git commit --amend` 或者 rebase 的方式修改掉。
不过并不推荐在提交之前手动做一次整个分支的 squash如果是 rebase 方式合并的话,也许更有意义。工蜂 (腾讯内部的 code 平台) 提供了 merge request 的标题和内容功能,所以没必要做 squash完全可以不必太聚合以便反应真实的信息。
为了不影响别人,只用它修改未 push 的 commit或者如果一条分支只有一个人你也可以修改已经 push 的 commit。
对于这条命令的更多功能,可以再去查阅其他文章。
## 可读的 graph 应该长什么样?
先说一个原则看graph要先看主线主线要清晰再看分叉上信息这与我们的工作流程是一致的。
![](rebase/6.png)
绿色的 hotfix 或者 feature 分支并不是每次只允许提交一次 commit只是这一段都是一些小更改。
这看起来有点可笑,一点都不高级。说了这么多做了这么多难道只是为了得到这么简单的图?
没错,`为了让东西变简单,本来就要付出很多代价`,我们所做的就是要让东西变简单,比如努力工作是为了让赚钱变简单,努力提升是为了让工作变简单。让事情变复杂只会让事情不可控。
当然具体如何还是要取决于你采用的 git flow但是原则很简单
* `每个分叉的子分叉尽量是一个串联一个,内部尽量不要再有自己的提交。`
为什么我认为这样的 git graph 可读性好,因为它把我们的工作也拍平了,不在乎每个工作的开始时间和持续时间,只关心这个工作的完成时间。
假如一个项目需求 1 是 1 月 1 号启动2 月 1 号上线,需求 2 是 1 月 20 号启动2 月 10 号上线。1 月 10 号修了一个 bug2 月 3 号修了一个 bug。听起来是不是很绕
如果你的 git graph 显示的也是这样的信息,可读性一定不好,所以我们要做的 git graph 应该反应的是如下信息:
* 1 月 10 号修补 bug
* 2 月 1 号上线需求 1
* 2 月 3 号修补 bug
* 2 月 10 号上线需求 2
## rebase 的缺点是什么?
这里并不讨论 rebase 可能带来的冲突问题,有很多文章都会讲,上面也已经提到了 rebase 的危险性,这里只讨论 rebase 对于 git graph 的缺点。实际上,冲突只是 rebase 不恰当使用导致的问题,而非 rebase 本身的问题。
当然也有人会说,工作的开始时间也很重要呀,因为它反映了当时工作开展的基础条件。对,这是 rebase master 的弊端。他让记录清晰,也让记录丢失了一些信息。记录的加工让可读性变得更好,也让信息量变少了。
git rebase 让 git graph 发生了变化,`每次分叉的检出和并入之间不会再有任何节点`。(因为合并到 master 采取的是 merge 行为。否则根本没有分叉)
![](rebase/7.png)
也就是这种情况不会再出现。因为每次总是 `rebase master`,把自己的起点抬了上去。`git rebase 实际上让检出信息没有意义,换取了主分支分叉的清晰。`
如果 rebase 没有缺点,那么也就没有争议。是否使用 rebase 也要看真实的需求是什么。
## 这篇文章要干什么?
通过rebase让git graph更可读。目的和原则我们都已经说过了没必要再重新说一遍。
多有谬误之处,还望不吝赐教!