LabHub
学习 学习路径 课程

Git 实战

变基就是 N 次合并

在 LabHub 中继续学习

一句话总结

提交对象中包含父提交哈希。父提交一旦改变,提交哈希必然改变。因此,rebase 不是移动提交的命令,而是重写提交的命令。

概念图: 不要重写别人正在查看的提交。 · 远程仍与自己最后一次看到的状态相同 · rerere · --onto

为什么需要它

“不要 rebase 已经推送的提交”这句话并不完全准确。关键不在于是否推送,而在于其他人的仓库中是否已经存在该提交对象,以及是否有人在它之上继续工作。因此,黄金法则的准确表述是:不要重写已经承载他人工作的提交。

为了整理只有自己使用的远程分支而执行 rebase 没有问题。真正的问题发生在别人已经基于该分支工作时。

它如何运作

相同冲突在 rebase 中反复出现,也是由其结构决定的。merge 只比较两个端点和共同祖先这三个位置;rebase 则逐个重新应用提交,每次都会重新执行一次三方合并。rebase 不是一次合并,而是 N 次合并。因此,rebase 五个提交时,可能会看到同一个冲突五次。启用 rerere.enabled true 后,Git 会记住首次解决冲突的方法,并在之后重新应用。

所谓冲突状态,是索引中同一路径存在三个候选版本(1 共同祖先 / 2 ours / 3 theirs)。这正是推荐使用 merge.conflictStyle zdiff3 的原因。只有知道双方共同从什么内容开始,而不只是对方修改了什么,才能正确合并。

交互式 rebase 有六个动作:pick(保留)、reword(只改消息)、edit(修改内容)、squash(合并内容与消息)、fixup(合并内容但丢弃消息)、drop(删除)。

--force-with-lease 会确认远程跟踪引用是否仍与自己最后一次看到的值相同。但它存在漏洞:如果 IDE 刚刚执行过 fetch,引用已经被更新,那么即使出现了自己从未看过的新提交,检查仍可能通过。可以用 push.useForceIfIncludes true 进行补强。

生产现场中的表现

rebase 发生冲突时,最常见的错误是直接选择完整的一侧。使用 --ours--theirs 快速跳过,会让另一方的工作悄无声息地消失。而且在 rebase 过程中,ours 与 theirs 的含义会让人感觉与直觉相反,因为正在重放的提交属于 theirs。

另一个常见问题是不知道如何中止。git rebase --abort 会准确恢复到开始前的状态。遇到阻塞时,与其勉强继续推进,几乎总是应该先中止并重新规划。

什么时候 rebase,什么时候 merge

规则只有一条:不要重写别人正在查看的提交。

场景 使用什么
把自己的本地分支移到最新 main 之上 rebase——历史更整洁
已经 push 的共享分支 merge——重写会破坏他人的历史
把 PR 合入 main 遵循团队规则(统一选择 squash、rebase 或 merge)
撤销已公开的内容 revert——通过新提交取消

如果必须重写已经推送的分支,应使用 --force-with-lease。与 --force 不同,它只会在远程仍与自己最后一次看到的状态相同时推送。如果期间有人推送了新内容,操作就会被拒绝,从而避免删除他人的提交。

减少冲突的工具

rerere——再次遇到相同冲突时,会自动应用之前的解决方法。多次 rebase 长期分支时尤其有用。

git config --global rerere.enabled true

--onto——只移动分支的根部。它适用于把从功能分支分出的另一个分支移动到 main 之上。

A---B---C  main
           D---E  feature
                       F---G  hotfix     ← D,E 없이 F,G 만 main 위로

git rebase --onto main feature hotfix

autosquash——根据评审意见修改时,先用 git commit --fixup <해시> 标记,再通过 git rebase -i --autosquash 一次性折叠。无需手动选择要合并到哪个提交。

发生事故后如何找回

即使在 rebase 中操作错误,通常也能恢复。Git 不会立即删除提交。

git reflog                    # HEAD 가 거쳐 온 모든 자리
git reset --hard HEAD@{5}     # 그중 하나로 되돌아간다

# 리베이스 중이라면 그냥 그만둘 수 있다
git rebase --abort

reflog 默认保留 90 天。所谓**“提交消失了”通常并非事实**,只是指向它的分支不存在了。只要知道哈希,就能找回。

git fsck --lost-found 甚至可以找到没有被任何引用指向的对象,是 reflog 中也不存在记录时的最后手段。

下一项实验要做什么

你将创建两个分叉分支,亲自比较 rebase 前后的哈希;通过快进生成直线历史;故意制造冲突,并在解决时保留双方修改;还会主动中止一次,确认恢复原状。随后使用交互式 rebase 把三个提交合并为一个并丢弃其中一个,最后用自己的语言总结黄金法则。