场景:
这是一个Git在日常工作中非常核心的问题,很多工作了多年的同事也没吃透这个问题,没有正确理解merge
和rebase
的区别。
今天我花时间自己做了几个实验,也算是明白了,记录一下。
常见场景
自己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开始,抓取新增加的改动 D
到 my-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
。