
1. Rebase到底是什么一次讲透先说个最直观的理解git rebase本质上做的事情就是把你当前分支上的提交摘下来重新放到另一个分支的最新提交后面。用一句大白话讲就是“把这段开发历史从原来的起点挪到新的起点上”。我见过太多刚开始接触git的开发者在终端里敲下git rebase master之后看到一大串提交被重放甚至看到“rebase in progress”这样的字样就慌了以为代码丢了。其实什么都没丢只是提交的“地基”变了。你可以把它理解成把一摞书从旧书架上搬到新书架上书的内容一字未动但摆放的位置和顺序跟以前不同了。这次我们重点讨论rebase是因为它在团队协作、个人分支整理、提交历史清洁度上价值确实非常大但它也是git里最容易让人“翻车”的命令之一。如果你对rebase的底层原理和操作边界理解不够深轻则产生一堆莫名其妙的冲突重则把共享分支的历史搞得一团糟甚至影响到别人的本地仓库。适合看这篇文章的人我觉得有两类。一类是已经开始用git、但一直只用merge、对rebase充满好奇又不敢碰的开发者另一类是已经在用rebase、但遇到冲突不会解决、或者不明白“为什么rebase之后推送总是被拒绝”的人。我会尽量把这些场景全部覆盖到。顺便提一句git安装和基础配置如果你还没搞定建议先把环境准备好。网上有很多“git安装及配置教程”比如Windows下直接下载安装包一路nextmacOS用brew install git装完至少执行一下git config --global user.name 你的名字 git config --global user.email 你的邮箱这两行不配置后面的提交都会失败或产生警告。下面所有内容我都会基于一个已经配置好git环境的前提来讲。2. 核心设计思路Rebase到底想解决什么问题2.1 提交历史的“线性化”执念先问一个问题为什么会有rebase这个命令原因很简单merge会在仓库里留下“分叉再合并”的痕迹而这种痕迹多了以后历史会变得非常难读。打个比方你用merge把feature分支合回mastergit会自动生成一个merge commit。这个merge commit有两个parent一个指向master的最新提交一个指向feature分支的最新提交。看起来就像两条河汇成一条。一条主干上有五六条这样的河汇入历史图就变成了一张网。很多人看这种网的历史第一反应是“这项目到底经历了什么”。rebase的出发点就是让历史尽量呈线性。feature分支从master分出来的时候基于某个commit A后来master前进了几个commit你用git rebase master把feature分支的提交重新在master最新commit之上重放。结果是feature分支看起来就像刚刚从master最新状态切出来的一样完全没有分叉。这在code review的时候非常舒服reviewer顺着提交一个个看下去就行不需要来回跳转对比。2.2 重放提交的工作原理rebase翻译成“变基”非常形象——改变的是“基”。它的底层逻辑分三步第一步找到当前分支和rebase目标分支的“分叉点”。这个分叉点就是两个分支共同的祖先commit。比如当前在feature分支执行git rebase mastergit会找出feature从master分叉出来的那个commit。第二步从分叉点之后一直到HEAD把当前分支的所有提交一个个记录下来。注意这里的“记录”不是简单复制commit内容而是打包成patch同时保留作者、提交时间、提交说明这些元数据。第三步把当前分支的HEAD移动到master的最新commit上然后在上面依次重放刚才记录的那些patch每重放一个就生成一个全新的commit。这就是为什么rebase之后commit的哈希值会变——提交内容一样但父提交变了hash自然跟着变。这里有一个重要的点必须讲清楚rebase过程中如果你的分支上有尚未push到远程的本地提交重放完成后这些提交的hash全部变化了。如果这些提交已经push到了远程而你又在rebase之后执行了强推git push --force那么所有其他人基于旧提交的本地工作都会出问题。这一点在团队协作中要极其谨慎后面我再细说。2.3 Rebase的“整理型”使用场景除了把分支变基到目标分支之上rebase还有一个常见用法就是整理自己分支上的提交历史。git rebase -i HEAD~n可以让你交互式地编辑最近n个提交包括pick保留某个提交原样reword保留提交内容但修改提交信息edit停下来修改提交内容或拆分成多个提交squash把当前提交合并到前一个提交drop直接删除某个提交这个功能在发Pull Request之前特别实用。你在开发过程中可能产生了十来个提交其中有“wip”、“fix typo”、“忘了改配置”这样的垃圾提交通过rebase -i把它们压缩成两三个逻辑清晰的提交reviewer看了会非常舒服。本质上这是用rebase在重构“你自己的工作历史”让它变得干净、有条理。当我第一次用squash把一个功能相关的7个提交合并成1个的时候那种历史瞬间变清爽的感觉真的会让人上瘾。不过rebase -i也最容易出问题因为你是真的在“动”提交操作前一定要想清楚最好先备份一个分支。3. Rebase与Merge的对比到底该怎么选3.1 两个命令的直观差异很多人纠结“rebase和merge到底用哪个”我先用一个例子把差异摆出来。假设master在commit C2你从C1分出了feature分支feature上产生了D1。同一时间master上产生了C3。如果用mergegit checkout feature git merge master结果会生成一个额外的merge commit Mfeature的历史是C1 - D1 - MM合并了C3。历史图上feature和master有一条分叉然后汇合。如果用rebasegit checkout feature git rebase masterD1会被重放到C3之后形成C1 - C3 - D1。没有merge commit历史是纯粹的直线但D1的hash变成了新的D1。merge保留了真实发生的“并行开发”痕迹rebase则把这个痕迹抹平让开发看起来像是串行的。两种各有取舍没有绝对的好坏关键是看你的团队更在意什么。3.2 团队协作场景下的推荐策略在团队协作中我个人的推荐策略也是很多成熟团队的通行做法可以概括成一句话远程共享分支上用merge本地私人分支整理上用rebase。更具体的做法是如果你在一个feature分支上开发想同步master上的最新代码优先用git rebase master这会让你的feature分支历史保持线性而且不会产生多余的merge commit。当你开发完成要合回master时用git merge feature或者通过Pull Request合并因为这时feature已经基于master的最新状态了合并过来就是一个clean的fast-forward不需要产生merge commit。很多人会担心rebase之后不就有了新的commit hash吗如果之后还要push怎么办这里的关键是时间点你rebase的是“尚未push到远程分支的本地提交”那一切安全如果你已经把feature分支push到了远程仓库并且和队友共享了这个分支那绝对不要去rebase它否则会给队友带来灾难性的同步问题。3.3 这么说可能更直观我常用一个类比来解释这两者的区别。merge像是你写了一篇论文还让另一个人在同一个文档的另一章同时写最后你们用Word的“合并修订”功能把两个版本合起来保留各自的编辑痕迹但文档里会出现各种各样的合并标记。rebase则像是你等对方先改完然后你再在他的最终版本上接着写。最终的文章看起来从头到尾都是一个人写的流畅、干净但你永远看不到“两人同时编辑”的那段历史了。你在团队里如果发现有人强推到共享分支上把这个分支的rebase结果直接覆盖到远程那基本等同于你在Word里把同事编辑过的部分全部重写了一遍还覆盖了他上传到共享网盘的版本这种操作轻则让人重新合并一次重则直接丢失工作成果。这个场景要特别小心后面问题排查部分我会再展开。3.4 表格对照Rebase与Merge的取舍清单对比维度RebaseMerge提交历史形态线性清爽便于阅读和review分叉合并真实但复杂是否产生额外merge commit不产生会产生除非fast-forward已有commit的hash会变所有重放后的新commit不变原commit保留冲突解决方式按提交逐个重放一个接一个处理一次性合并统一解决对远程分支的影响强推会破坏他人同步普通push即可安全适合场景本地提交整理、未推送分支同步上游共享分支合入、长期分支集成这张表基本覆盖了我在实际工作中做取舍时考虑的要素。核心判断标准就一条这个分支是“我的私有工作区”还是“团队的公共区域”。私有的随便rebase公共的老老实实merge。4. 实操全流程从基础变基到交互式整理4.1 第一步基础rebase的标准操作先演示最常见的场景——同步上游master到我的feature分支。# 确保当前在feature分支 git checkout feature # 先拉取master最新状态 git fetch origin master # 把feature分支变基到origin/master之上 git rebase origin/master这里我特意用了origin/master而不是本地master是为了确保你基于的是远程仓库的最新状态因为你自己本地的master可能已经落后了。很多人图省事直接git rebase master但本地master如果好久没更新rebase的基就不够新后面还可能再出冲突不如一开始就基于origin/master。执行过程中git会按顺序重放你的提交。如果一切顺利终端会输出“Successfully rebased and updated refs/heads/feature”这样的提示。如果遇到冲突git会停下来进入rebase的中断状态提示你当前处于REBASE模式你可以用git status查看冲突文件。4.2 第二步冲突解决的标准姿势假设rebase过程中出现了冲突git会停下来。这时候不要慌解决问题的流程是第一步用git status查看哪些文件冲突了。冲突文件会出现在“Unmerged paths”下面。第二步打开冲突文件你会看到这样的内容 HEAD 这里是目标分支比如origin/master的代码 这里是你的提交里的代码 你的提交哈希和说明注意上下分别是两边的代码。你需要手动保留正确的版本把、、这些标记全部删掉。第三步保存文件然后执行git add 冲突文件注意这里不需要也不能执行git commit。rebase过程中的冲突解决和merge不一样merge解决完冲突要commitrebase只需要git add标记为已解决然后继续git rebase --continuegit会把当前这个提交重新提交然后继续重放下一个。如果有多个提交都冲突这个过程可能要重复多次。这里还有一个很实用的小技巧git rebase --continue之后如果有冲突git会弹出commit message编辑器因为有时rebase过程中提交说明会被拼接修改。如果你不想改直接:wq退出vim即可。4.3 第三步交互式rebase整理提交历史再说说git rebase -i的实操。这个命令我基本天天用特别是在发Pull Request之前。假设我的功能分支上有最近4个提交其中有几个想合并、一个想改提交信息git rebase -i HEAD~4执行后git会打开一个编辑器列出最近4个提交从上到下按时间从旧到新排列每行格式是pick 7f3a9d1 feat: 添加订单模块 pick 3b2c8e4 wip: 完善订单逻辑 pick 9a1d5f2 fix: 修订单金额计算 pick e4c6b7a feat: 添加订单测试用例每个提交前面可以改成不同的指令。比如我想把后面三个提交合并到第一个就改成pick 7f3a9d1 feat: 添加订单模块 squash 3b2c8e4 wip: 完善订单逻辑 squash 9a1d5f2 fix: 修订单金额计算 squash e4c6b7a feat: 添加订单测试用例保存退出后git会重新打开编辑器让你编辑合并后的提交说明。我把提交说明统一成一句“feat: 添加订单模块及测试用例”这样一个功能就只有一个提交了。这里要特别提醒如果你在执行rebase -i后想中途取消全部操作可以用git rebase --abort这会撤销所有rebase操作恢复到rebase之前的状态。但如果已经--continue走了一半再abort就只能回到rebase前的起点中间已经处理好的提交可能会丢所以abort一定要尽早用。4.4 防止失手的备份策略在我们讨论更复杂的情形之前先给大家一个保命原则操作rebase之前先在当前分支打一个临时备份分支。操作层面非常简单git branch backup/my-feature-before-rebase这样即使rebase过程中把提交历史搞坏了你也能从backup/my-feature-before-rebase分支找回原来的状态。我刚开始用rebase时吃过不少亏有了这个习惯之后心理压力明显小了很多。另一个思路是如果你担心rebase后的历史有问题可以先记录一下当前HEAD的哈希万不得已时用git reset --hard 原哈希回退。4.5 还没有push的提交要rebase谁来“背锅”这里有位同事踩过一个坑我拿来当反面教材讲。他没拉最新master之前直接开发了一个新功能本地产生了三个提交然后他觉得应该先去拉master所以执行了git pull origin master最终在本地产生了merge commit。等他push时发现远程被拒因为他本地master落后远程好几个版本pull下来的merge把两个互不相干的历史扭在一起远程自然拒绝。正确的做法应该是git fetch origin master git rebase origin/master而不是git pull。要用一条命令同时兼顾fetch和rebase的话可以直接用git pull --rebase origin master--rebase参数就是告诉git“拉取后不要merge而是rebase我的本地提交到远程新提交之上”。这条命令在远程和本地提交都还没有互相覆盖的场景下非常安全历史就是一条干净的线。5. Rebase操作中的常见问题与排查技巧5.1 为什么rebase之后push总被拒绝这是出现频率最高的问题多见于本地rebase成功、push却报错“non-fast-forward”的场景。报错的原因是远程分支上已经有一些提交不是基于你的本地提交历史了而你rebase后本地提交的hash全部变化git认为你试图“重写”远程分支历史。解决办法是确认这个远程分支是否允许强推。如果这是一个feature专用分支且只有你在用可以git push --force-with-lease origin feature我用--force-with-lease而不用--force是因为前者会在推送前检查远程分支是否被其他人更新过——如果更新过它会拒绝推送避免你不小心覆盖队友刚提交的代码。这个参数是git团队专门为这种场景设计的本质上是给强推加了一道保险丝。如果远程分支是共享的比如大家共用的develop分支你绝对不能强推。这种情况下唯一的处理路径是跟团队沟通或者放弃rebase改用merge。5.2 遇到“rebase in progress”卡住如何安全退出rebase过程中如果遇到你解决不了的冲突或者你自己搞不清方向了最稳妥的撤法就是git rebase --abort这会撤销整个rebase操作让分支回到执行rebase之前的状态。我见过不少人因为冲突太多一步步按照提示手动解决结果越解越乱最后想回到初始状态都找不到路。如果你确认不想继续了git rebase --abort是你的安全出口。另一种情况你在rebase --continue之前不小心执行了别的命令git提示“No rebase in progress”这可能是因为你已经在某个步骤意外提交了。这时先看git status如果想彻底恢复先找rebase前的分支备份点用git reset --hard 备份分支回到备份状态再重新开始。5.3 交互式rebase时想拆分一个提交怎么做这个操作很多书上都没详细讲。场景是你有一个提交里混杂了两个功能想拆成两个逻辑独立的提交。操作方法是在git rebase -i的编辑列表里把那个提交前面的pick改成edit保存退出。git会停下来停在那个提交应用完成之后。这时候执行git reset HEAD^注意这个reset是mixed模式会把HEAD指向上一个提交但保留工作区文件。所以那个提交的所有改动都变成了未暂存状态。接下来你就可以用git add -p或者普通git add逐步分功能add然后分别git commit拆成多个逻辑清晰的提交。拆完后执行git rebase --continue继续后续重放。这个技巧我用了很多次非常实用。但我要强调它只适用于还没有push到远程的提交已经push过的提交不要这么折腾否则团队同步会出大乱子。5.4 常见问题速查表现象常见原因处理方式rebase后push被拒绝本地hash重写与远程不一致用--force-with-lease或改用merge冲突反复出现一个提交解决一次rebase逐提交重放每个提交通通要处理耐心理清每个提交的改动不要想着跳过错误地执行了git commit解决冲突rebase中的冲突不需要手动commit用git rebase --continue替代想退出rebase思路不清或冲突太多git rebase --abort回到rebase前状态rebase后想找回丢失的提交误操作reset或drop用git reflog找到原哈希git reset --hard交互式rebase后提交说明不对忘记修改commit messagegit commit --amend或再执行一次rebase -i5.5 Reflogrebase误操作的最后防线说实话即使我反复强调备份还是有人不备份就直接rebase然后误删了提交。这时候git reflog就是最后一根救命稻草。reflog记录的是你本地仓库中HEAD引用在每次变动时指向的commit哈希包括rebase、reset、甚至某些看起来不可逆的操作。操作方式git reflog你会看到一长串记录类似f3a1b2c HEAD{0}: rebase finished: refs/heads/feature onto 3d9e8c1 d4e5f6a HEAD{1}: commit: fix: 修订单金额计算 9a1d5f2 HEAD{2}: commit: wip: 完善订单逻辑如果你想恢复到rebase之前的HEAD{2}状态执行git reset --hard 9a1d5f2这个操作会把你带回原来的提交位置相当于rebase白干了。我把它当成“撤销的撤销”几乎每个git用户都该掌握。有个原则值得记住在git里只要你的本地操作没有执行gc清理大部分丢失的提交都可以用reflog找回来。5.6 使用rebase要注意的三个雷区第一个雷区绝对不要在共享分支上执行rebase然后强推。前面已经反复强调这里再提一次是因为后果真的太严重。一旦你把rebase后的分支强推上去所有基于旧提交开发的其他成员pull时都会报错他们不得不进行一遍痛苦的灾后重建。第二个雷区rebase合并后的历史不要再用git push正常推要用--force-with-lease。很多新手把rebase做完以后顺手git push被拒后一脸懵然后换git push --force虽然推上去了但没有任何保护存在覆盖队友代码的风险。正确姿势是养成用--force-with-lease的习惯。第三个雷区不要在git rebase -i里随便drop掉一个提交除非你确定它的改动已经包含在另一个提交里。drop意味着那个提交的内容会彻底消失如果后续发现丢了一部分功能就得靠reflog去找。我建议即使是drop也先记录一下那个提交的hash。6. 实际工作流中的rebase实践6.1 个人项目中的rebase习惯在个人项目中我的工作流往往非常依赖rebase。因为个人项目没有团队协作的压力我可以放心大胆地整理自己的提交历史。比如我开发一个新功能时经常会产生这样的提交节奏先搭框架一个commit写一半突然发现配置写错了又fix一个commit写完主体后又发现某个参数名不统一再refactor一个commit。这种提交历史如果直接推到远端自己看都觉得乱。所以在push之前我会执行类似这样的操作git rebase -i origin/master把所有wip、fix typo这类提交合并成有意义的逻辑提交。个人项目的整个历史看起来就像一本书的目录每一个功能、每一个修复都有明确的章节而不是一堆流水账。6.2 团队协作中的rebase纪律团队协作中rebase的纪律比技巧更重要。我在团队内部定的几条规矩第一feature分支不要长期存在开发完尽快合回主干减少需要rebase同步上游的机会。第二如果你需要rebase同步上游master一定先git fetch origin再git rebase origin/master不要直接用本地master因为本地master可能已经落后。第三feature分支一旦push到远程并已经让别人拉取过就再也不要rebase。这时候想同步master用git merge origin/master虽然会多一个merge commit但安全性高很多。第四如果确实出现了必须rebase共享分支的情况比如你们团队坚持线性历史且特性分支是多人共享的那要提前通知所有成员让他们先提交并推送自己的本地工作然后统一在某一个时间点由专人执行rebase和强推其他人同步时用git fetch和git reset --hard origin/feature来对齐。这种操作要求极高的协调度新手团队不要轻易尝试。6.3 结合git pull --rebase管理本地与远端同步日常开发中最常见的一个场景是我和队友同在一个feature分支上工作队友已经推了提交我本地也有一些未推送的提交。这时候直接git pull会生成一个merge commit历史里突然多出一个“Merge remote-tracking branch”的提交看着很碍眼。我习惯用git pull --rebase origin featuregit会先把我的本地提交放在一边拉取远方最新代码再把我的本地提交重放上去。整个过程如果没冲突历史就是一条平平整整的直线引入了一个新知识点--autostash参数。如果你本地有未提交的改动未暂存或已暂存但还没commit直接pull --rebase会报错因为git不想带着未提交的改动去重放提交。但如果你加了--autostashgit会先把你的临时改动stash保存起来rebase完成后再自动恢复git pull --rebase --autostash origin feature这个参数我现在基本成了习惯尤其是频繁切换分支、工作区经常有临时改动的场景下可以少踩不少“pull失败请先提交或stash”的坑。6.4 一些值得尝试的rebase扩展技巧还有一些rebase能顺便做到的事情分享给大家。如果你只是想修改上一个提交的message不用进交互式rebase直接git commit --amend在打开的编辑器里改完保存即可。这个操作本质上也是rebase的一种因为它会生成一个新的commit替换旧commit。如果你有个提交已经推上远程但你要改它的内容尽快git commit --amend然后重新push不过依然要用--force-with-lease并确保这个分支只有你一个人用。还有一个很实用的技巧git rebase --onto。它的场景是你从master分出了A分支又从A分出了B分支B上做了一堆工作。现在你想把B分支上的改动平移到master上不要A中间的那些提交这时候git rebase --onto master A B意思就是把B上从A分叉点之后的提交重放到master上。这个命令比较冷门但确实是rebase家族里很能解决实际问题的成员。7. 我踩过的一些坑以及想对新人说的话7.1 第一次强推带来的教训有一次我把一个feature分支rebase到了最新master上本地历史很漂亮。我推的时候直接在VSCode的Push按钮上点了一下结果被拒。然后我一时心急从终端执行了git push --force。推送成功了我以为事情结束了。结果第二天队友私聊我“我本地一直在同步这个分支今早pull的时候一堆冲突我重新对着你的强推分支reset了一下花了不少时间。”那一刻我才明白远程分支不等于我的私有草稿。后来我给自己立了个规矩**只要这个分支还有其他人可能在使用就绝不强推如果实在要推先问过团队。**用--force-with-lease也是从这个教训里学到的它不会真的杜绝风险但至少能防止我盲目覆盖别人的提交。7.2 交互式rebase时手滑把提交删了怎么办另一次是在跑rebase -i时本来想并行处理一个小的fix commit结果一个手滑把edit写成drop保存退出后发现那个提交的改动从历史里彻底消失了。幸好我当时刚执行完rebase立刻用git reflog看到了操作前的哈希一条git reset --hard就回来了。从那以后我除非明确要删除某个提交否则“drop”这个指令我都很少碰更多的是用squash去合并。7.3 关于“rebase还是merge”的最终建议最后想说说观点层面的事情。网上关于rebase和merge谁更好的争论可以一天一夜不重样但我个人的体会是**工具本身没有优劣效率和风险都体现在你怎么用、在什么场景用。**我见过完全不用rebase的项目历史里全是merge commit也乱糟糟的但人家一样维护得好我也见过强制要求只用rebase的团队如果成员不熟练冲突和个人错误的代价很高。真正优秀的git使用者不是那种永远只用一条命令的人而是能根据分支的性质、团队的协作深度、发布节奏的紧缓在rebase和merge之间快速做判断的人。如果你能用好两者你提交给reviewer的历史会干干净净你在团队里的协作口碑也会提升一大截。根据我个人的经验最稳的成长路径是先在个人项目里把rebase的各类操作练熟尤其学会rebase -i、--force-with-lease、--autostash和reflog救助这套组合拳。再逐步把这些技能带入团队先从不共享的feature分支开始慢慢建立一套大家都舒服的工作规范。等哪一天你可以在冲突面前面不改色地快速处理完那就说明你对rebase的理解已经非常到位了。