
git rebase 大概是 Git 里头争议最大的一条命令没有之一。有人把它当整理提交历史的利器有人视它为破坏协作的洪水猛兽两边都能搬出一堆理由谁也说服不了谁。我见过不少团队有人害怕 rebase 的根本原因其实不是命令本身难而是没搞懂它底层到底做了什么结果一碰冲突、一碰已推送的分支就手足无措。这篇就把 rebase 从底层机制到日常操作再到我踩过的坑和团队协作的约定一次性聊透。1. 先把话说清楚rebase 到底改了什么1.1 它不是移动提交是重写历史很多人第一次用 rebase直观感觉是哦把我的提交挪到别人提交后面。这个理解方向没错但漏掉了一个关键事实rebase 不是把提交移动过去而是把你原来的提交全部丢弃重新生成一批全新的提交。举个例子你在 feature 分支上基于 main 的 C1 提交做了三个提交C2、C3、C4。此时 main 上多了新提交 C5。你执行git rebase main结果看起来像是 C2、C3、C4 依次排在 C1 和 C5 后面。但这些 C2、C3、C4 已经不是原来的提交了——它们是新对象有新的 commit hash、新的时间戳、新的父提交。你原来的 C2、C3、C4 还在仓库里躺一段时间直到被 GC 回收只是没人再引用它们了。这正是 rebase 和 merge 最本质的区别。merge 是合并成果它保留双方的历史再新增一个合并节点把两条线接起来rebase 是改写历史它把你这条线上的提交在别人最新的成果上重新演一遍结果是一根直线但代价是历史被重写了。1.2 底层三步走理解这个才能懂冲突rebase 的本质是对目标分支上的每个提交做一次自动化的 cherry-pick。具体分三步把 HEAD 切到目标分支的最新提交上。找出当前分支上从分叉点之后产生的所有提交。逐个把这些提交的 diff 应用apply到新基点上。应用过程中一旦和已有内容冲突git 就会暂停等你手工解决后git rebase --continue继续。可以这样类比你在一个移动的货架上摆放箱子。原货架位子已经变了main 往前走了rebase 就是先把你的箱子全部搬下来等货架挪到新位置后再按原顺序把箱子重新码上去——码的过程中如果新位置已经有别人的东西挡路你就得先把挡路的东西挪开这就是冲突。理解了底层机制你就会明白一个关键推论rebase 后你的每个提交的内容可能和原来一样但提交身份已经完全不同。所以任何依赖原提交身份的东西比如别人已经基于这个提交做了后续开发在 rebase 后都会出问题。这就是已推送分支不要 rebase这条铁律的根本原因。1.3 为什么 rebase 出来的是直线这个不用展开太多因为每个提交都被重新铺到最新基点上不会产生一个叫 Merge Commit 的、带有两个父提交的特殊提交所以git log --graph画出来就是一条直线。线性历史追查起来确实方便尤其看 blame、看 log、做 code review 时一条线顺着捋下来非常舒服。但我们必须承认这条直线是新历史不是原历史。它抹掉了并行开发的真实过程让所有工作看起来像是一条流水线生产出来的。这个特性既是卖点也是 rebase 被诟病的地方——你整理的其实不是代码是叙事。2. rebase 和 merge 的世纪之争到底怎么选2.1 先看它们各自在历史上留下的痕迹merge 和 rebase 解决的是同一个问题把另一个分支的工作整合到当前分支。但它们在历史里留下的手印完全不同。merge 会保留两个分支的原始分叉结构历史图是一张网。每个分支的提交真实地待在自己原本的位置另外多出一个合并提交把两边的成果接在一起。优点是真实谁在哪天做了什么都清清楚楚缺点是主线、支线交错看的成本越来越高。rebase 则是抹平分叉历史是一条直线。优点是干净、好读缺点是整体形态不再是真实发生过的事。对比维度mergerebase历史形态网状分叉线性原提交对象保留原提交额外新增合并提交重建出新提交原提交被丢弃冲突解决次数一次可能多次每个提交都可能撞一次冲突对已推送分支操作安全危险会破坏他人本地历史保留真实开发过程保留不保留看起来像一次连续开发分支图可读性越合越乱始终整齐2.2 我的选型原则本地随便 rebase共享分支只用 merge我这些年带团队总结了一个简单粗暴还管用的分法个人本地分支、尚未推送的提交随便 rebase甚至鼓励你用rebase -i把提交整理得逻辑清晰。已经推送、别人可能已经拉下来用过的分支不要 rebase老实 merge。这条原则至少能避开 90% 的 rebase 事故。你本地分支是你自己的草稿纸你随便擦改一旦推送出去了远端的历史就是团队公共财产任何人都不该单方面改写。有人会问那 Linux 这类开源项目不也大量 rebase 吗没错但它们的工作流本身就是patch 提交制——贡献者把补丁发上去维护者 rebase 到主干上这些提交从出生起就没有被大众共享过重写是安全的。普通团队日常共享开发分支情况完全不同历史一旦被重写轻则别人 pull 出重复提交重则本地出现鬼打墙式的分叉得让全组人花半天擦屁股。2.3 rebase 让历史更好看的真相好看不等于真实。我见过一些团队强制全员 rebase结果大家为了把提交压成一条线先在本地拼命写wip、fixup、oops之类的垃圾提交信息最后再统一 squash 掉。这种为了好看而强行编排的做法反而让历史记录失去了参考价值。我更倾向于把 rebase 用在对的位置它是用来整理还没推出去的提交的让每一笔提交逻辑完整、可以直接进 code review一旦提交推出去被别人用上了就承认分叉、允许分叉该 merge 就 merge。这样既有了好看的开发历史又不会惹出协作事故。3. 日常实操rebase 的正确打开方式3.1 第一件事把 git pull 默认策略改成 rebase大多数人应该先改掉这个习惯用git pull --rebase替代默认的git pull或者直接设置全局配置git config --global pull.rebase true默认的git pull等价于git fetch git merge。在团队共享分支上每个人本地都经常比远端旧于是每次 pull 都在制造一个Merge remote-tracking branch噪音提交。时间一长主干历史的 log 里全是这种毫无信息量的提交真正有用的提交被淹没在里面。改成 rebase 策略后pull 等价于git fetch git rebase。git 会先把你的本地提交放到一边把远端最新提交拉下来当新基线再把你本地提交逐个应用回去历史最后是一根干净的线。我改了这个配置后再也没回过默认策略。日常最舒服的一条命令其实是配合自动暂存git pull --rebase --autostash--autostash解决的是工作区不干净时 rebase 直接拒绝执行的问题。如果你手头有未提交的改动加了这个 flaggit 会先自动 stash、再自动 pop全程一条命令完成。我个人的日常同步流程就是这一条命令简洁且没有噪音。3.2 交互式 rebase整理提交的必修课git rebase -i是 rebase 家族里最值钱的东西。交互模式下每个提交前面有几个操作可以选pick保留该提交reword保留提交内容但修改提交信息edit停下来允许修改提交内容或把它拆成多个提交squash把该提交合并到前一个提交中fixup类似 squash但直接丢弃该提交的提交信息drop删除该提交经典场景开发一个功能的时候随手提交了三次git log --oneline看到1122abc 修复第三个 bug 3344def 修复第二个 bug 5566ghi 修复第一个 bug这类打死一只苍蝇式的零散提交直接推上去很业余。想合成一笔实现 XX 功能就执行git rebase -i HEAD~3编辑器打开后把后两行的pick改成squashpick 5566ghi 修复第一个 bug squash 3344def 修复第二个 bug squash 1122abc 修复第三个 bug保存退出后再编辑合并后的提交信息最后得到一笔干净提交。这里有个细节几乎每个初学者都踩交互式列表是从旧到新排列的要把最新提交并进最旧提交你得改下面行的关键字而不是改最上面那行。我第一次就把顺序搞反结果提交信息乱成一锅粥。3.3 进阶用法用 rebase -i 拆提交rebase 不仅能合还能拆。把某行pick改成edit保存退出后 git 会停在该提交处然后# 先撤销提交但保留所有文件改动 git reset HEAD~ # 再按逻辑重新分笔提交 git add src/utils.ts git commit -m 抽取工具函数 git add src/api.ts git commit -m 实现接口调用 # 继续向下执行 git rebase --continue这种先合后拆的组合是把一段混乱开发过程整理成逻辑链的利器。我自己开发时经常随手提交到了收尾阶段统一把分散的提交重排成需求实现、测试补充、文档更新三笔。Review 的同事看到这样的提交结构基本不用我去解释这些改动为什么在一起。4. 我在生产环境踩过的 rebase 的坑4.1 同一个文件的冲突为什么要解好几遍merge 冲突通常只解一次rebase 冲突可能解好几遍。原因不复杂rebase 是逐提交应用的如果好几个提交都动了同一块代码而新的基线里这块代码也被改动过那么每应用一个提交都可能撞一次同样位置的冲突。我有次负责一个配置重构在 feature 分支上连写了五个提交每笔提交都对同一个 config 文件加配置。不巧 main 上另一位同事的改动也动了这个文件。rebase 到一半这个文件我前后解了三次冲突心态差点爆炸。后来我把习惯改了在 rebase 之前先把自己的零散提交 squash 成一两笔再执行 rebase。这不仅是让历史好看更是缩小冲突面。提交越碎、改动越交叠rebase 的冲突次数越多。先合再 rebase冲突面就是完整功能 vs 完整功能而不是碎片切片 vs 完整功能。实操上还有一个保命技能环境太乱时不要硬扛git rebase --abort及时撤退回退到 rebase 之前的状态再冷静对比下两边到底改了些什么。4.2 --autostash 不是银弹小心改动蒸发--autostash是很方便但它有个肉眼可见的坑stash 在 rebase 完成后的自动 pop阶段如果发生冲突你的未提交改动会和 rebase 后的代码搅在一起需要在工作区里手工解决。这时候如果手快不小心执行了git checkout .或者git stash drop那些未提交的改动就永远找不回来了。我的建议autostash 只适合少量改动、大概率不冲突的场景。如果本地堆着大量未提交的改动我会先手动git stash push把 rebase 和 stash pop 分成两步走每一步都能确认结果正常再进行下一步。少打一行字的代价可能是丢失半天的工作量不划算。4.3 已推送分支 rebase 的事故现场还原这条最狠绝对是 rebase 最大的坑没有之一。假设你推送了一个 feature 分支到远端同事小明基于它拉了个自己的开发分支。然后你本地把 feature 分支 rebase 了主分支再强制推送到远端。小明那边 pull 的时候会发现本地历史里既有旧基线上的提交、又有新基线上的提交git 会报各种诡异冲突甚至要求他用 merge 去整合两个看起来差不多的提交。真实事故现场长这样你git push --force-with-lease origin feature 小明git pull # 冲突炸裂一大早心态崩了 小明git pull --rebase # 发现提交列表里出现一大堆重复提交正确的处理分两种。如果你确实必须重写已推送历史比如提交里不小心塞了密码、大文件、敏感配置那一定提前通知所有可能 pull 这条分支的人约好让他们重新 clone 或 fetch reset。如果只是受不了提交太乱想整理我建议换个更温和的手段不要 rebase 已推送分支直接把整理好的新分支换个名字推上去让旧分支原地退休。另外无论哪种情况都别用git push --force。请用git push --force-with-lease。后者推送前会检查远端是否还是你上次 fetch 时的状态如果远端已经被别人推了新提交它直接拒绝覆盖等价于多了一层保险。4.4 rebase 中途误操作后怎么把代码捞回来有一回我遇到一堆冲突脑子一热执行了git rebase --skip结果一个提交的改动被整个跳过了代码当场消失。后来才反应过来--skip不是跳过冲突而是跳过这个提交。它设计出来的用途是让你丢弃那些已经被其他提交包含了的重复改动。如果跳错了git rebase --abort能救——但前提是你还没--continue。如果已经走完流程只能靠 reflog 捞git reflog # 找出 rebase 之前那个提交的 hash git reset --hard hashreflog 是一个被低估到离谱的命令它记录了这个仓库所有引用变更的历史哪怕提交被 rebase 重写过只要还是本机操作基本都能靠 reflog 救回来。从那之后我养成了一个成本几乎为零、但关键时刻能续命的习惯rebase 之前先记下当前分支 hash甚至直接打个临时 taggit tag backup-before-rebase。一有意外一条命令回到起点比在冲突堆里懊恼强太多了。5. 团队协作里rebase 的规矩怎么定5.1 把分支分成三类分别给策略我带团队时喜欢把分支分成三种类型分别规定 rebase 权限规则越简单越容易被遵守分支类型rebase 策略个人本地临时分支随便 rebase鼓励用rebase -i整理提交团队共享开发分支dev、test 等禁止 rebase只允许 merge主干分支main/master禁止直接提交、禁止 rebase只能通过 PR/MR 进入这个约定解释了本地是你的私人草稿纸共享历史是公共财产的逻辑。个人分支上你怎么折腾都行一旦多人共享任何单方面改写历史的操作都是破坏。5.2 我最推荐的团队工作流开发端 rebase主干端 merge具体到中小团队我最推荐这套流程从 main 切出 feature 分支编码在本地自由提交。push 之前用rebase -i把历史压缩成逻辑清晰的 2~5 笔提交。执行git pull --rebase --autostash同步 main 最新代码解决冲突后 push。提交 PR/MR进入 code review。维护者合入时使用--no-ff保留一个合并提交。第 5 步很多人会忽略但--no-ff非常关键。它保证 main 上每个功能都有一个明确的合并节点后面要 revert 某个功能只需要 revert 那个 merge commit整个功能的改动就被完整移除不会残留半截代码。这套流程的好处是开发者的本地历史被 rebase 整理得干净线性主干历史因为有 merge commit 保留了功能粒度的聚合点两头的好处都占了。5.3 别把 rebase 当炫技我见过一些团队把用 rebase 把历史改得乱七八糟再花式还原当成高手行为在群里晒各种骚操作。说实话把底层命令玩得转确实说明功力但团队协作里最重要的东西是可预测——所有人应该知道历史大致长什么样知道每条命令按下去会发生什么。我给团队定的最后一条规矩是rebase 只允许出现在两个时机——第一还没推送的本地整理阶段第二从主干同步最新代码时的pull --rebase。其他场景一律 merge。规则短就好记好记就好遵守好遵守协作的自然事故就少。6. 几个 rebase 相关的高频疑问6.1 rebase 和 cherry-pick 到底是什么关系很多人绕不清这两个命令。一句话说明rebase 就是自动化的连续 cherry-pick。它把你分支上的每个提交按顺序逐一 cherry-pick 到目标基线上。所以理解 cherry-pick是理解 rebase 机制的前提。如果你想手动把某个分支的某一笔或某几笔提交搬到当前分支git cherry-pick commit-1 commit-2如果你想搬一段连续的提交区间用 rebase 的 --onto 变体更合适。比如把 dev 分支上从 A 到 B 之间的所有提交搬到 feature 分支git rebase --onto feature A B这条命令的语义是把 B 里面、A 不包含的那些提交搬上去。--onto 是 rebase 里最灵活但也最绕的用法能把它熟练用起来的基本可以告别对 rebase 的恐惧了。6.2 rebase 冲突里莫名其妙出现整个文件都变了有朋友说 rebase 时某个文件明明没人改过git 却报 conflict。这种问题十有八九是换行符CRLF/LF和空白字符差异导致的Windows 混合开发环境下特别常见。可以配置git config --global core.autocrlf true # Windows 用户 git config --global core.autocrlf input # macOS/Linux 用户更根本的解决方式是在仓库根目录加.gitattributes统一各类文本文件的换行符策略。这玩意儿配置好之后能帮你省掉一大堆周六早上莫名其妙冲突的烦心事。6.3 IDE 里怎么可视化做 rebase命令行之外IDEA 系列的交互式 rebase 也做得很人性化。在 Log 视图里选中一个目标提交右键选 Interactively Rebase from Here就能拖拽排序、右键选 squash/edit/drop操作所见即所得。我建议新手先在 IDE 里玩一遍它会实时生成对应的命令行输出做完之后对照 git window 里的命令很快就能把底层逻辑悟透。等回到终端环境时你会发现自己已经不是只会敲指令的复读机了。6.4 rebase 进行到一半接到紧急任务怎么办这个场景我曾经处理过多次rebase 撞了一堆冲突正处理到一半任务要求马上切换到另一个分支处理线上问题。传统做法是 stash 然后切分支但 rebase 中途的状态是不能随便 stash 的很容易把半成品状态搞混。这时候git worktree是真正的救星git worktree add ../project-hotfix hotfix cd ../project-hotfix # 在独立的 worktree 里处理紧急修复完全不干扰原分支它能在同一个仓库里开出多个并行工作目录每个目录对应不同分支。rebase 半途、冲突未解决的状态就安稳地留在原目录里等你处理完紧急事务再回来接着解。这套组合在多人协作、多任务切换频繁的环境下价值极大。聊到最后说点我个人的体会。Git 这套工具链之所以让人又爱又恨是因为它的设计逻辑偏向底层机制完全暴露每个命令都由多个基础操作组合而成。rebase 尤其如此——它把所有提交都当可以重写的草稿给了你极强的整理能力但也要求你对什么能改、什么不能改有清醒认识。我最后的建议就一句话本地提交随便改共享历史不要动。只要守住这条线rebase 就是整理提交历史最趁手的工具用完你会忍不住把以前那些乱糟糟的分叉线全部收拾一遍然后再也回不去默认 merge 的 pull 了。