git rebase有个坑

这是个老问题,以前没注意..

场景:

这是一个Git在日常工作中非常核心的问题,很多工作了多年的同事也没吃透这个问题,没有正确理解mergerebase的区别。
今天我花时间自己做了几个实验,也算是明白了,记录一下。

常见场景
自己fork了一个分支进行一个特性功能的开发,开发完了准备发起了PR
结果发现在自己开发期间,主分支有了几次新的合入。

这时候你想把主分支的改动更新到本地。
为了让合并的历史更优雅, 此时执行了git reabase upstream main
此时问题来了,你会发现你的这个本地的分支push不上去了。

原因
简单来说,git rebase 操作修改了你本地分支的提交历史,使其与远程分支的提交历史产生了分歧。
Git为了保护远程分支不被意外覆盖,会拒绝你的non-fast-forward推送。

正常的 git push 流程

在没有冲突的情况下,git push 遵循一个快进式(Fast-forward)的原则。

远程分支 (origin/my-feature) 的历史是:

A — B

你拉取了代码,在本地 (my-feature) 继续工作,增加了提交 C:

A — B — C

当你执行 git push 时,Git会比较你的本地分支和远程分支。
它发现你的本地分支只是在远程分支提交B的基础上加了一个提交C
于是它会执行一次Fast-forward,直接把远程分支的指针移动到C
推送后,远程分支也变成了:

A — B — C

这个过程是最常见的,也是安全的,因为它只是在原有历史的末尾继续添加新内容,不会丢失任何东西。

git rebase 的流程解释

rebase 的中文意思是变基,它的核心作用是重写提交历史,让分支历史变成一条直线更美观。

假设你从 main 分支切出了 my-feature 分支并开始工作。

main 分支: A — B

my-feature 分支: A — B — C (你增加了提交 C)

在你工作的时候,你的同事向 main 分支推送了一个新的提交 D。

main 分支现在是: A — B — D

你的 my-feature 分支还是: A — B — C

此时,你的分支和 main 分支从提交 B 开始分叉了。
为了让你的分支包含 main 的最新更改,你执行了 git rebase main

rebase 会做以下事情:
a. 暂时"收起“你在 my-feature 分支上的独有提交(也就是 C)。
b. 从与 main 分支最后的共同提交B开始,抓取新增加的改动 Dmy-feature 分支
c. 将刚才收起的提交 C 在新的起点 D 上重新应用一遍。

此时my-feature 分支变成了: A — B — D — 收起的C

关键点来了:
重新应用的 C 这个提交,虽然代码内容没变,但它的父提交从原来的 B 变成了现在的 D
在Git中,一个提交的唯一标识SHA-1哈希值是由其内容、作者、时间戳、以及父提交等信息共同决定的。
父提交变了,哈希值就会变!所以新的 C 对应的hashID,和原来的 C 是不同的
你实际上得到一个内容完全一样,但是hashID变了的提交 C'

所以Rebase之后,my-feature 分支历史: A — B — D — C'

为什么 rebase 后 push 会失败?

现在,我们来比较一下 rebase 后的本地分支和远程分支

本地 my-feature 分支: A — B — D — C'

远程 origin/my-feature 分支: A — B — C

当你执行 git push 时,Git会进行比较,然后它会发现: 这两个分支从共同的祖先 B 开始就分道扬镳了。本地 my-feature的历史里并没有包含远程的 C 提交。
如果接受推送,远程的 C 提交就会丢失,这太危险了!所以拒绝这次推送。

这就是你看到的 (non-fast-forward) 错误。
Git通过这个机制,防止你无意中覆盖掉远程仓库可能存在的、你本地没有的提交。

解决方式:

方式1 在使用fork后的分支开发后,使用merge策略来合并改动。
缺点: commit的历史线会比较混乱,不好看

方式2 使用reabse后,搭配push -f来强行更新远程自己的分支
Commit ID的历史会是一条直线,就像前面例子的A --- B --- D --- C',很会优雅

解释push -f 的作用和风险

git push --force (或简写 -f) 就是你给Git下的一个强制命令,意思是:

“别管什么快进不快进了,也别管远程分支上有什么。我push给你的这个版本就是最终版本,你就用我这个版本去覆盖”

执行 git push -f 后,

远程的 origin/my-feature 被强制更新为: A — B — D — C'

git push -f 是一个比较危险的操作,千万不要向公共分支(如 main, develop)执行 push -f

对于自己的特性分支执行是没有问题的。通常,一个特性分支只有你一个人在开发。
在你准备合并到主分支之前,用 rebase 来保持分支的整洁,然后用 push -f 更新你自己的远程分支,这是非常常见的做法。

更安全的选择:git push --force-with-lease
它在强制推送前会增加一个检查:只有当远程分支的状态和你本地最后一次拉取时一模一样,它才会执行强制推送。

换句话说,如果在你执行 rebase 到你准备 push 的这段时间里,有其他人也向这个远程分支推送了新的提交, --force-with-lease 就会失败。这可以防止你覆盖掉别人在你不知情的情况下推送的工作。 日常工作中,推荐使用 git push --force-with-lease 代替 git push -f

Licensed under CC BY-NC-SA 4.0
最后更新于 2025-08-02 15:47:29
使用 Hugo 构建
主题 StackJimmy 设计