Git rebase 变基
rebase(变基)是 Git 里最强大也最让新手畏惧的命令。它和 merge 都能合并分支,但思路完全不同:merge 是"把两条线汇到一起",rebase 是"把一条线的起点挪到另一条线的末尾",让历史变成一条直线。这一章讲清楚 rebase 的原理、交互式 rebase 的妙用,以及那条不能违反的黄金法则。
1. rebase 是什么
场景:你从 main 拉了 feature 分支开发,期间别人往 main 提交了新代码。现在你的 feature 落后于 main。git rebase main 会把你 feature 上的提交"摘下来",接到 main 最新位置的后面,就像你是在最新 main 基础上开发的一样。
# rebase:把当前分支的起点"挪"到目标分支最新位置
# 场景:你在 feature 开发时,main 已经被别人更新了
# 合并前(有分叉):
# A - D - E (main)
# \
# B - C (feature)
#
git switch feature
git rebase main
# Git 把 B、C 摘下来,接到 E 后面,重新生成 B'、C'
# rebase 后(历史"拉直"了):
# A - D - E - B' - C' (main, feature)
#
# B' C' 是新的提交,哈希变了(因为是"重新播放"出来的)
# 如果有冲突:
# 1. Git 暂停,提示哪些文件冲突
# 2. 手动解决冲突文件
# 3. git add 解决后的文件
git add .
git rebase --continue # 继续下一个提交
# 中途想放弃:
git rebase --abort # 回到 rebase 前注意:rebase 会改写提交哈希——B 变成 B',C 变成 C',因为它们是"在新基底上重新播放"出来的新提交。内容一样,但身份证号变了。
2. rebase vs merge
这是 Git 协作里永恒的话题。两者没有绝对优劣,理解区别后根据场景选择:
# rebase vs merge 的本质区别
#
# 用 merge 合并 feature 到 main:
# A - D - E - M (main, M 是合并提交)
# \ /
# B - C --- (feature)
# 历史保留了"分叉"的痕迹,真实但杂
#
# 用 rebase 后再 merge:
# A - D - E - B' - C' (main, feature)
# 历史是一条直线,干净
#
# 总结:
# merge :保留分叉历史,产生合并提交(真实,但历史有岔)
# rebase :把提交"拉直",历史是一条线(干净,但改写了提交哈希)
#
# 团队约定好规则即可。常见做法:
# - 个人 feature 分支开发时,经常 rebase 主干,避免分叉太远
# - 最终合并回主干时,用 merge 或 squash merge 保留功能边界一个常见的折中策略:个人 feature 分支开发期间经常 rebase 主干(保持和最新代码同步、减少最终冲突),合并回主干时用 merge 或 squash(保留功能边界,方便日后追溯"这个功能是哪个 PR 加的")。
3. 交互式 rebase — 整理历史的神器
普通 rebase 只是把分支挪位置,而 git rebase -i(interactive)能让你逐个审视提交:压扁、改 message、删掉、重排顺序。这是 push 之前"美化历史"的最强工具。
# 交互式 rebase:整理历史的最强工具
# 场景:push 之前,把 feature 上的多个提交"整理"一下
git rebase -i HEAD~4 # 整理最近 4 个提交
# Git 会打开编辑器,列出这 4 个提交:
# pick a1b2c3d feat: 添加登录表单
# pick e4f5g6h fix: 修了个 typo
# pick i7j8k9l wip
# pick m0n1o2p feat: 添加校验
#
# 把 pick 改成下面的动作:
# pick(p) 保留(默认)
# reword(r) 保留提交但改 commit message
# squash(s) 把这个提交合并到上一个(共用 message)
# fixup(f) 同 squash 但丢弃这个的 message
# drop(d) 删除这个提交
# edit(e) 暂停,让你修改这个提交的内容
# reorder 调整行的顺序 = 调整提交顺序
#
# 示例:压成 1 个干净的提交
pick a1b2c3d feat: 添加登录表单
fixup e4f5g6h fix: 修了个 typo
drop i7j8k9l wip
fixup m0n1o2p feat: 添加校验
# 保存退出后,Git 把 4 个提交整理成 1 个(登录表单)典型用途:
- 压缩提交:把"wip"、"fix typo"等零散提交 squash 成一个干净的"feat: 添加登录"。
- 改写 message:把之前写得太随便的 commit message 改规范。
- 删掉误提交:不小心提交了密钥/大文件,rebase 掉它(前提是还没 push)。
- 拆分提交:一个提交做了两件事,用 edit 暂停后拆成两个。
4. rebase 的黄金法则
绝对不要 rebase 已经 push 到公共分支的提交。
原因:rebase 会改写提交哈希。如果同事已经基于你的旧哈希在工作,你 rebase 后他们的历史就对不上了,pull 时会遇到噩梦般的冲突——同样的改动出现两份,Git 无法自动合并。
安全的做法:
- 私人 feature 分支随便 rebase:只有你一个人在用,改了哈希也没事。
- 已 push 的分支要 rebase:用
git push --force-with-lease(不是-f),并且确认没人正在基于它工作。 - main / develop 等公共分支永不 rebase:只能往里 merge,不能改写历史。
5. 让 pull 自动 rebase
很多人不喜欢 git pull 产生的"Merge branch main"提交——明明只是同步代码,却污染了历史。配置一下让 pull 默认用 rebase:
# 让 pull 自动用 rebase(避免无意义的 merge 提交)
git config --global pull.rebase true
# 之后 git pull 等价于 git pull --rebase
# 设置默认只 rebase 当前 push 出去的分支(安全)
git config --global rebase.autoStash true
# rebase 前自动 stash 工作区改动,rebase 完自动 pop
# 查看当前 rebase 配置
git config --get pull.rebase
# GitHub 网页合并时选 "Rebase and merge"
# 也能保持 main 历史线性(不产生 merge 提交)6. rebase 中断与恢复
rebase 过程中遇到冲突会暂停,你会看到提示。几个常用操作:
git rebase --continue:解决冲突后继续。git rebase --skip:跳过当前提交(如果这个提交其实没必要)。git rebase --abort:放弃整个 rebase,回到开始前。- 如果搞砸了:
git reflog能看到所有操作记录,reset 回 rebase 之前的状态(见"撤销操作"篇)。
新手建议
- 先学 merge 再学 rebase:merge 足够日常用,理解 rebase 后再用更高效。
- 在练习仓库里试:rebase 的"改写历史"概念抽象,自己建个仓库多试几次就懂了。
- 配 autoStash:rebase 前自动暂存工作区改动,省心。
- 团队约定清楚:是否允许 rebase、是否用 force-with-lease,写进团队规范。
← 上一篇 Git 工作流
下一篇 Git stash 暂存 →