183 lines
11 KiB
Markdown
183 lines
11 KiB
Markdown
使用 rebase 打造可读的 git graph
|
||
===
|
||
原文:https://juejin.cn/post/6844903818136666125
|
||
|
||
## git graph 可读指什么?
|
||
|
||
这里的可读,主要指的是能够通过看 git graph 了解每一次版本更迭,每一次 hotfix 的修改记录。反映到分支上面,有两个要求:
|
||
|
||
* 每个分支的历史修改可读 (单个分支的层面)
|
||
* 每个分支的分叉合并可读 (多个分支的层面)
|
||
|
||
## rebase 是什么,它是更优雅的 merge 吗?
|
||
|
||
rebase 翻译做 `变(re)基(base)`。
|
||
|
||
讲 rebase 的文章经常会引用三张图:
|
||
|
||

|
||
|
||
原本的两个分支
|
||
|
||

|
||
|
||
通过 merge 的结果
|
||
|
||

|
||
|
||
通过 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 的顺序`。
|
||
|
||

|
||
|
||
比如这里,使用 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 的主线会变得丧失了主线的意义 (因为它太单薄了,很多工作根本没反应上来)。
|
||
|
||

|
||
|
||
比如这里,本来左数第二条玫红色的才是主线,因为不规范地在 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要先看主线,主线要清晰,再看分叉上信息,这与我们的工作流程是一致的。
|
||
|
||

|
||
|
||
绿色的 hotfix 或者 feature 分支并不是每次只允许提交一次 commit,只是这一段都是一些小更改。
|
||
|
||
这看起来有点可笑,一点都不高级。说了这么多做了这么多难道只是为了得到这么简单的图?
|
||
|
||
没错,`为了让东西变简单,本来就要付出很多代价`,我们所做的就是要让东西变简单,比如努力工作是为了让赚钱变简单,努力提升是为了让工作变简单。让事情变复杂只会让事情不可控。
|
||
|
||
当然具体如何还是要取决于你采用的 git flow,但是原则很简单:
|
||
|
||
* `每个分叉的子分叉尽量是一个串联一个,内部尽量不要再有自己的提交。`
|
||
|
||
为什么我认为这样的 git graph 可读性好,因为它把我们的工作也拍平了,不在乎每个工作的开始时间和持续时间,只关心这个工作的完成时间。
|
||
|
||
假如一个项目需求 1 是 1 月 1 号启动,2 月 1 号上线,需求 2 是 1 月 20 号启动,2 月 10 号上线。1 月 10 号修了一个 bug,2 月 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 master`,把自己的起点抬了上去。`git rebase 实际上让检出信息没有意义,换取了主分支分叉的清晰。`
|
||
|
||
如果 rebase 没有缺点,那么也就没有争议。是否使用 rebase 也要看真实的需求是什么。
|
||
|
||
## 这篇文章要干什么?
|
||
|
||
通过rebase让git graph更可读。目的和原则我们都已经说过了,没必要再重新说一遍。
|
||
|
||
多有谬误之处,还望不吝赐教!
|