打开网易新闻 查看精彩图片

最近跟前同事聊了聊天,一个有趣的见闻,说他们公司禁止git使用rebase~,但是日常的git使用这不都是很常用的命令吗?也没看到啥风险呀~相信很多朋友一开始也有这样的疑问~

1 merge与rebase原理

先不急着说谁好谁坏,现把原理搞清楚再判断,假设你现在有个 main 分支,上面已经有了提交 D→E→F→G。然后你从 E 节点切了一个 feature 分支出来,在上面提交了 A、B、C 三次。

那么 merge 和 rebase 分别会怎么处理这段历史呢? 一张图能说明白绝不用两张:

打开网易新闻 查看精彩图片

如上图所示可以看到, git merge 的做法很简单粗暴:它不会动你 feature 分支上的 A、B、C,而是在 main 上额外创建一个叫"merge commit"的 节点(H) ,把两个分支的历史合在一起。原来的分叉结构完整保留,谁在什么时候做了什么一目了然。

git checkout main
git merge feature
# 自动生成一个 merge commit (H)

git rebase 的思路完全不一样:它把 feature 分支上的 A、B、C 三个提交"拆下来",临时保存,然后把 feature 的基底挪到 main 的最新节点 G 上 ,再把刚才拆下来的提交逐个"贴回去",生成全新的 A'、B'、C'。

注意,这里 A' 和原来的 A 虽然改动内容一样,但 commit hash 完全不同 ——因为 parent 指针变了,时间戳也可能变了。在 Git 眼里,这就是三个全新的提交。

git checkout feature
git rebase main
# A、B、C 变成 A'、B'、C'(hash全部改变)

如果 feature 分支只有你一个人在用,且从来没 push 过,那 rebase 完直接 git push 就完事了,历史非常干净,一条直线。可一旦这个分支已经被推到远程,而且有别人在基于它开发——那就麻烦大了,这个后面细说。

2 区别

merge合并 ——保留所有人的提交历史原封不动,多一个 merge commit 把两条线汇到一起。分支历史是"有分叉"的。

rebase变基 ——把你的提交"搬"到目标分支的最新点,重写 commit hash。分支历史变成" 一条直线 "。

记住五个字的差别就行了: merge 不改历史,rebase 改历史。

merge 不挑场合,公共分支、私人分支都能用,冲突一次性解决,历史保留完整分叉——谁在什么时候合入了什么,清清楚楚。代价就是分支树会比较"花",我一开始也不习惯,慢慢看惯了还好。

rebase 的优势是历史干净,一条直线,但代价也明显:冲突要逐个 commit 解决、commit hash 全变了、而且 绝对不能对已推送的公共分支用 ——这个不是"不建议",是"禁止"。

好了,就最后这一句"禁止对公共分支 rebase",你应该隐约感觉到为什么有些公司直接禁掉这个命令了。

3 糟糕

上面说了,rebase 会生成全新的 commit hash。如果你 rebase 的分支只有自己用、且从未推送,那完全没问题——这也是 rebase 的正确用法。

可一旦你对一个 已经推送到远程且已被别人拉取 的分支做 rebase,然后 force push,情况就变成了这样:

打开网易新闻 查看精彩图片

同事 A 把 feature 分支 push 到了远程(上面的 A-B-C)

  • 同事 B 基于这些 commit 继续开发,本地也有一份 A-B-C 的历史

  • 同事 A 突然 rebase 了 feature ,然后 git push --force ,远程的 A-B-C 变成了 A'-B'-C'

  • 同事 B 想 push 自己的代码,Git 直接报错 "non-fast-forward rejected"

  • 同事 B 一脸懵逼地 git pull ,结果拉下来一堆莫名其妙冲突——而且还是 别人的 commit 引起的冲突

  • 最后 B 的本地历史变成 A-B-C-A'-B'-C'-Merge 一团乱麻,想回滚都不知道回哪里

  • 说实在的当团队规模一上来,这种事发生一次就够喝一壶的。轻则浪费半天排查,重则 CI 流水线因为找不到 commit 直接挂掉,代码 review 记录全乱。

    所以那些所谓"禁止 rebase"的公司,其实基本上禁的都是 对已推送的公共分支做 re base + force push

    如果团队里 Git 水平参差不齐,一刀切"都别用 rebase 了"其实也是无奈之举,毕竟稳定比整洁重要。