[git技巧]一次 `git push` 被拒绝的真实经历:我该 merge 还是 rebase?
记录一次看似普通、却让我真正理解 Git 分支历史的经历。

一、问题现场:一次“很正常”的 push
那天我只是给代码加了一点 debug 日志,流程非常熟练:
git add ml/train_cnn.py
git commit -m "debug: add logs"
git push
结果,Git 给了我一个并不友好的回复:
! [rejected] kylin/3.3.1-demo -> kylin/3.3.1-demo (fetch first)
error: failed to push some refs to 'http://xxx/algorithm/DeepLearning.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally.
我心里第一反应是,我只是加了点日志, 没改接口、没动依赖, 为什么连 push 都不让我了?
于是我照提示执行了:
git pull
结果事情变得更“抽象”了。
二、Git 开始说人话,但我一时没听懂
git pull 的输出里,最关键的是这一句:
You have divergent branches and need to specify how to reconcile them.
fatal: Need to specify how to reconcile divergent branches.
再一看状态:
Your branch and 'origin/kylin/3.3.1-demo' have diverged,
and have 1 and 20 different commits each.
这句话信息量其实很大,但当时我并没有完全意识到。

三、什么叫 “branches have diverged”?
我们先把 Git 的话翻译成人话。
这句:
have 1 and 20 different commits each
-
意思是:
- 远端分支:比我本地多 20 个提交
- 本地分支:比远端多 1 个提交
-
也就是说,历史已经不是一条线了,而是这样:
A --- B --- C --- D --- ... --- T (origin) \ E (local)E:我刚提交的debug: add logsT:远端最新提交
-
Git 不知道该怎么把这两条历史“合并成一条”
-
所以它停下来,问我一句话:“你希望历史最后长什么样?”
四、Git 给我的三个选项,分别意味着什么?
Git 的提示里,其实已经给了答案:
git config pull.rebase false # merge
git config pull.rebase true # rebase
git config pull.ff only # fast-forward only
这三种方式,本质上回答的是同一个问题:当本地和远端的提交历史已经分叉时, 你希望 Git 如何“整理这段历史”?
下面我们一个一个来看。
4.1 merge:最安全,但最“吵”
git pull --no-rebase
这是 Git 最保守、也最符合直觉 的做法。
(1) 流程
-
它到底做了什么?
当你执行 merge 时,Git 不会去“重写”任何已有提交,而是:
- 保留远端的提交历史
- 保留你本地的提交历史
- 额外创建一个 merge commit,把两条分叉的历史连接起来
-
历史结构会变成这样:
A --- B --- C --- ... --- T --- M \ / E ---------------------T:远端最新提交E:你本地的新提交M:Git 自动生成的 merge commit
(2) 优缺点
- 优点
- ✅ 绝对安全:不会丢提交
- ✅ 不会修改任何已有 commit 的 hash
- ✅ 非常适合对 Git 机制不熟的团队
- merge 的代价
- ❌ 会产生额外的 merge commit
- ❌ 提交历史不再是“一条线”
- ❌
git log、git bisect、git blame可读性下降
尤其是在你只改了一点点代码的时候,一个 “Merge branch …” 提交,显得有些“用力过猛”。
4.2 rebase:最干净,但需要你明白在干什么(⭐ 推荐)
git pull --rebase
相比 merge,rebase 的思路完全不同。

(1) rebase的核心思想
不要保留“分叉”这件事本身,只保留“最终发生过哪些修改”。
-
Git 会做一件很巧妙的事:
- 把你本地的提交 暂时移除
- 先把分支移动到远端最新提交
- 再把你的提交 按顺序重新应用一遍
-
最终历史会变成:
-
A --- B --- C --- ... --- T --- E从结果上看,就像是你是在远端最新代码的基础上,直接提交了 E
(2) 优缺点
- 优点
- ✅ 提交历史完全线性
- ✅ 没有多余的 merge commit
- ✅ 非常适合:
- 小改动
- debug
- 文档修正
- 算法实验代码
- 缺点
- ⚠️ rebase 会重写提交历史
- ⚠️ 如果发生冲突,需要你亲自解决
- ⚠️ 不要对已经被多人依赖的公共提交 rebase
- 一句话总结:rebase 很优雅,但它假设你知道自己在做什么。
4.3 fast-forward only:理想但不现实
git pull --ff-only
fast-forward 是 Git 最“理想化”的模式。
-
它的要求非常苛刻:
- 本地没有任何独立提交
- 本地分支只是落后于远端
-
也就是说,历史必须是这样:
A --- B --- C --- D (origin) ^ localGit 才能简单地把指针“向前挪一格”。
一旦你本地已经有提交(哪怕只有一个),ff-only 就会直接拒绝操作。
要求本地没有任何独立提交,在真实项目中几乎用不上。
因此在真实的多人协作项目中:fast-forward more 是一种**“理论上很好,实践中很少遇到”的模式**
五、我为什么选择 rebase?
理解了三种策略之后,选择其实就变得非常自然了。
-
结合我当时的实际情况:
- ✔ 只改了 一个文件
- ✔ 只是 debug 日志
- ✔ 不想污染主干历史
- ✔ 不希望多一个 merge commit
-
在这种场景下:
保留“分叉发生过”这件事,本身没有任何价值。
我真正关心的只有一件事:最终代码 + 一条干净、可读的提交历史
-
因此,rebase 是最合理、也最克制的选择。
六、最终的实战操作(非常简单)
6.1 rebase 到远端最新分支
git pull --rebase origin kylin/3.3.1-demo
如果一切顺利,这一步会直接完成。
6.2 如果出现冲突,Git 会明确告诉你
如果有冲突,Git 会提示你:
git status
Git 会列出冲突文件,并标记冲突位置。
解决冲突后:
git add 冲突文件
git rebase --continue
Git 会继续“回放”剩余的提交。
6.3 如果你发现自己走错了路
如果你发现自己改错了,也可以随时撤退:
git rebase --abort
这条命令会把仓库恢复到 rebase 之前的状态,不会留下任何“半成品历史”。
6.4 push
git push origin kylin/3.3.1-demo
此时,本地历史已经是远端的“自然延续”,push 会顺利通过。
七、事后的一点反思
这次经历让我意识到一件事:Git 并不是在为难你,它只是在一个关键节点上,要求你对“历史形态”做出选择。
-
merge:强调安全
-
rebase:强调整洁
-
ff-only:强调理想状态
理解这一点之后,Git 的很多“奇怪报错”都会变得合理。
八、一个我后来做的改进(推荐)
为了不再每次被 Git 问一遍,我后来直接设置了默认策略:
git config --global pull.rebase true
从此,git pull = git pull --rebase,历史默认保持线性,再也不慌 divergent branches
九、总结一句话
当 Git 提示 branches have diverged 时,它真正的问题不是:**“你要不要 pull?”**而是: “你希望你的代码历史,看起来像什么?”
更多推荐


所有评论(0)