
1. 为什么我们需要rebase从一次真实的合并“车祸现场”说起我猜你点开这篇文章多半是因为被git merge后那错综复杂、像蜘蛛网一样的提交历史给搞烦了。或者你正准备向一个活跃的开源项目提交PR维护者礼貌地回复你“请先rebase到最新的master分支。” 你一头雾水去搜教程看到的要么是晦涩的原理图要么是一堆吓人的命令感觉一不小心就会把代码库搞炸。别担心我刚开始用git rebase的时候跟你的感受一模一样。它就像Git里的一个“危险”工具人人都在用但没人敢轻易碰。直到有一次我在一个长期开发的功能分支上眼睁睁看着因为频繁的git merge提交历史变成了一个理不清的毛线团我才下定决心要彻底搞懂它。简单来说git rebase的核心就一句话“重新定义基准点”。想象一下你从主路比如master分支的一条岔路口比如feature分支开始修一条新路。修路期间主路本身也在向前延伸。git merge的做法是在你修的新路和现在的主路尽头之间直接架一座桥把两条路连通。这样历史记录里会明确保留“这里曾经有过岔路”的事实。而git rebase的做法更“激进”它把你新修的这条路整个“平移”到当前主路的最新起点上假装你从一开始就是在最新的主路上开始修的。这样最终的历史就是一条完美、笔直的直线看不到任何分叉的痕迹。那么到底该用哪个这取决于你的团队文化和项目状态。如果你在维护一个公共的、历史清晰的开源项目或者团队强调提交历史的整洁性那么git rebase是首选。如果你更看重保留完整的协作上下文或者分支合并非常复杂那么git merge更安全。今天我们就来彻底驯服git rebase这头“猛兽”让它为你所用。2. 图解rebase从“架桥”到“移山”的本质转变要理解rebase我们必须先把它和merge放在一起对比看。很多教程一上来就讲命令但如果不理解背后的“时空观”你永远会感到困惑。2.1 经典场景feature分支的开发与合并假设我们有一个简单的仓库初始提交是C0。你基于master分支此时在C0创建了一个feature分支准备开发新功能。C1---C2---C3 (feature) / C0---C4---C5 (master)你在feature分支上辛勤工作了几天提交了C1,C2,C3。与此同时你的同事在master分支上合并了一些其他更新产生了C4和C5。现在你的feature分支的“基准”还是古老的C0而master已经跑到了C5。此时如果你执行git merge你切换到master分支然后执行git merge feature。Git 会找到一个最佳的“共同祖先”C0然后创建一个新的“合并提交”C6。这个C6有两个父节点C5和C3。历史图会变成这样C1---C2---C3 / \ C0---C4---C5---C6 (master)你得到了一条清晰的合并记录但也引入了一个分叉点。如果这样的分支很多历史图就会像地铁线路图一样复杂。此时如果你执行git rebase你切换到feature分支然后执行git rebase master。这时Git 会进行一系列神奇的操作找到分歧点Git 找到feature分支和master分支的共同祖先C0。暂存差异Git 计算出从C0到C3feature分支的尖端这一路上所有提交C1,C2,C3引入的代码变更并把它们暂时保存起来。重置指针将feature分支的指针“快进”到master分支的最新提交C5上。注意此时C1,C2,C3在feature分支上暂时“消失”了。重新应用提交Git 把刚才暂存的那些变更以C5为新的基础一个一个地重新“提交”上去。因为基础变了这些重新生成的提交会有全新的提交ID比如C1,C2,C3但变更内容和你原来的一模一样。最终的历史图会变成这样C1---C2---C3 (feature) / C0---C4---C5 (master)看feature分支的起点从C0“平移”到了C5历史变成了一条直线。现在你再切回master执行git merge feature因为feature的所有提交都在master的“前方”Git 会执行一次“快进合并”fast-forward直接把master指针移到C3连合并提交都不会产生C0---C4---C5---C1---C2---C3 (master, feature)一条无比清晰、线性的历史就此诞生。这就是rebase的魅力也是它让历史保持整洁的秘密。2.2 黄金法则什么时候绝对不能用rebase理解了原理就必须牢记一条git rebase的黄金法则永远不要对已经推送到远程仓库的提交进行rebase。为什么因为rebase的本质是“重写历史”。它创建了新的提交C1,C2,C3来替代旧的提交C1,C2,C3。在你的本地仓库这没问题旧提交会被垃圾回收。但是如果你的旧提交已经推送到了像 GitHub、GitLab 这样的远程仓库并且可能已经被其他同事拉取pull到了他们的本地仓库。这时你再强行推送push重写后的历史就会导致你的本地历史和远程历史对不上。其他同事再拉取时会陷入一场混乱的合并冲突或者看到大量“重复”的提交旧的和新的都在。要解决这种问题非常麻烦需要强制推送git push -f而这会覆盖别人的工作是团队协作中的大忌。所以请把这句话刻在脑子里rebase只适用于你本地、尚未推送的提交。对于已经共享的提交老老实实用merge。3. 手把手实操从零开始玩转rebase光说不练假把式。我们用一个最简单的例子从头到尾操作一遍让你亲眼看到每一步发生了什么。请打开你的终端跟着我一起做。3.1 准备实验环境首先我们创建一个干净的实验目录并初始化Git仓库mkdir rebase-demo cd rebase-demo git init创建一个初始文件并提交作为我们的master分支起点echo Initial content file.txt git add file.txt git commit -m C0: Initial commit现在我们模拟在master上有了新的提交比如同事的更新echo Update from master branch file.txt git add file.txt git commit -m C1: Update on master此时我们的master分支有两个提交C0和C1。历史是线性的。3.2 创建并开发feature分支假设现在你要开发一个新功能。基于C0创建并切换到一个新分支feature# 我们先回到C0以此为基础创建feature分支 git checkout -b feature HEAD~1 # HEAD~1 表示当前提交C1的上一个提交即C0注意这里我们特意基于C0HEAD~1创建分支而不是基于最新的C1就是为了模拟真实开发中你从某个较早的节点开始工作而主分支已经前进的场景。在feature分支上我们做两次提交echo Feature work A file.txt git add file.txt git commit -m F1: Feature work A echo Feature work B file.txt git add file.txt git commit -m F2: Feature work B现在我们来看一下提交图。使用git log --oneline --graph --all* f123456 (HEAD - feature) F2: Feature work B * abcdef0 F1: Feature work A | * 7890abc (master) C1: Update on master |/ * 3456789 C0: Initial commit看得很清楚master在C1feature分支从C0分叉出去有了F1和F2两个提交。这就是我们rebase前的状态。3.3 执行rebase操作我们的目标是让feature分支基于最新的master即C1。切换到feature分支如果还没在的话然后执行rebasegit checkout feature git rebase master你会看到类似这样的输出First, rewinding head to replay your work on top of it... Applying: F1: Feature work A Applying: F2: Feature work B这个过程就是前面原理部分讲的Git 先把feature分支的指针暂时回退到和master的共同祖先C0然后把F1、F2的变更暂存再将feature分支快进到masterC1最后把暂存的变更依次应用上去生成新的提交F1和F2。现在再看提交图* a1b2c3d (HEAD - feature) F2: Feature work B * d4e5f6a F1: Feature work A * 7890abc (master) C1: Update on master * 3456789 C0: Initial commit太棒了历史变成了一条完美的直线。feature分支的起点现在紧挨着master的最新提交C1。注意提交IDa1b2c3d,d4e5f6a已经和之前的f123456,abcdef0完全不同了它们是全新的提交。3.4 完成合并快进Fast-Forward现在feature分支的代码已经包含了master的最新内容并且我们的工作是基于最新代码进行的。我们可以将feature分支合并回master。切换到master分支然后合并git checkout master git merge feature因为feature的所有提交都在master的“前面”Git 会执行一次快进合并输出Fast-forward。master分支的指针直接移动到了feature分支所在的位置。最终的历史图使用git log --oneline --graph* a1b2c3d (HEAD - master, feature) F2: Feature work B * d4e5f6a F1: Feature work A * 7890abc C1: Update on master * 3456789 C0: Initial commit一条清晰、线性、易于阅读的历史记录诞生了。这就是一次完整的、标准的rebase工作流。4. 进阶技巧与实战避坑指南掌握了基础操作我们来看看rebase更强大的用法以及那些教程里不会告诉你的“坑”。4.1 交互式rebase整理你的提交记录git rebase -i交互式rebase是代码整理的瑞士军刀。它可以让你在“重新应用提交”的过程中对提交进行排序、合并、拆分、修改提交信息等操作。这在准备一个干净的PR时尤其有用。假设我们在feature分支上有三个提交F1: Add user login functionF2: Fix typo in login pageF3: Add user logout function其中F2只是修复了F1里的一个拼写错误完全没必要作为一个独立提交。我们可以用交互式rebase将它们合并。# 假设当前在feature分支我们要整理最新的3个提交 git rebase -i HEAD~3这会打开一个编辑器如Vim或VSCode显示如下内容pick a1b2c3d F1: Add user login function pick d4e5f6a F2: Fix typo in login page pick e7f8g9h F3: Add user logout function每一行代表一个提交前面的pick表示“应用这个提交”。我们可以修改这些命令将第二行的pick改为squash或简写s表示将这个提交合并到前一个提交中。也可以改为fixup或f效果类似squash但会直接丢弃这个提交的日志信息。修改后pick a1b2c3d F1: Add user login function squash d4e5f6a F2: Fix typo in login page pick e7f8g9h F3: Add user logout function保存并退出编辑器。Git 会应用这些更改并可能再次打开编辑器让你为合并后的新提交编辑提交信息。完成后原来的三个提交就变成了两个一个包含了登录功能和拼写修复的提交和一个独立的登出功能提交。历史瞬间变得干净利落。实操心得交互式rebase是本地提交的“后悔药”。在推送到远程之前大胆地用它对提交历史进行美容。常用的命令还有reword修改提交信息、edit暂停rebase允许你修改提交内容、drop删除提交。多练习几次你就会爱上这种掌控历史的感觉。4.2 处理rebase过程中的冲突rebase并不是魔法。当你的修改和新的基础master上的修改发生在同一文件的同一区域时冲突就会发生。这与merge时遇到冲突本质相同但处理方式略有区别。假设在执行git rebase master时在应用某个提交比如Applying: F1: Feature work A时发生了冲突Git会停下来并告诉你Auto-merging file.txt CONFLICT (content): Merge conflict in file.txt error: could not apply abcdef0... F1: Feature work A Resolve all conflicts manually, mark them as resolved with git add/rm conflicted_files, then run git rebase --continue. You can instead skip this patch with git rebase --skip. To abort and go back to the original state, run git rebase --abort.处理流程如下不要慌。Git已经暂停了rebase过程等待你解决冲突。解决冲突打开冲突文件这里是file.txt你会看到标准的冲突标记,,。根据需求手动编辑文件保留你想要的代码删除冲突标记。标记冲突已解决解决完所有冲突文件后用git add file将文件标记为已解决。或者用git add .添加所有更改。继续rebase运行git rebase --continue。Git 会创建这个冲突提交的新版本基于你解决后的内容然后继续应用后面的提交。可选跳过或中止如果这个冲突的提交你完全不想要了可以用git rebase --skip跳过这个补丁。如果冲突太多想放弃整个rebase操作回到最初的状态用git rebase --abort。这是你的安全绳。避坑指南与merge一次性解决所有冲突不同rebase是逐个提交应用变更。这意味着你可能会在多个提交上遇到冲突需要重复“解决-add-continue”的过程。虽然繁琐但这也有个好处你可以确保每一个提交在应用到新基础后都是独立且正确的保持了每个提交的原子性。解决冲突时务必仔细阅读冲突内容判断是保留你的更改、采用他人的更改还是进行融合。4.3 使用--onto进行更灵活的重基这是rebase的一个高级但极其有用的功能。假设你的分支结构更复杂A---B---C (topic) / D---E---F---G (master) \ H---I (next)你基于next分支创建了topic分支A基于E。但现在你想让topic分支基于master分支而不是next。直接git rebase master会把E、F、G的变更也包含进来这不是你想要的。这时就需要--ontogit rebase --onto master next topic这个命令的意思是“把topic分支上但不在next分支上的提交即A, B, C重新应用到master分支上。” 结果会变成A---B---C (topic) / D---E---F---G (master) \ H---I (next)topic分支干净地“移植”到了master上。这个命令在需要调整分支基准或者从一个旧的长周期特性分支中“拯救”出部分提交时非常有用。5. Rebase vs Merge如何根据场景做出正确选择到了这里你已经是一个rebase高手了。但高手不仅要会用它还要知道什么时候不用它。我们来做一个最终的对比和总结。选择git merge当你需要保留完整的历史上下文特别是当分支的生命周期很长或者多人协作非常频繁时那个合并提交本身就是一个重要的历史标记记录了“何时、为何进行了这次集成”。分支合并非常复杂如果两个分支的修改已经严重分叉进行rebase可能需要解决大量冲突且可能破坏原有提交的原子性。一次合并提交更能清晰地记录这次复杂的集成。你对历史线性化没有强迫症有些团队或项目并不介意历史图看起来像一棵树他们更看重记录的完整性。选择git rebase当你在准备提交PR/MR之前这是rebase最经典的场景。在将本地特性分支推送到远程并创建拉取请求前先rebase到主分支的最新状态。这能确保你的变更是在最新代码基础上测试的也让维护者更容易评审和合并因为历史是线性的。你追求清晰、线性的项目历史像Linux内核这样的项目就要求提交历史必须是一条直线。这便于使用git bisect等工具进行问题定位。你在整理本地提交使用交互式rebase来合并琐碎的提交、修改错误的提交信息让每一个提交都是一个独立、完整、有意义的变更集。我个人的工作流习惯对于我个人的项目或我主导的团队我倾向于采用一种混合策略可以称之为“rebase-merge”工作流本地开发在本地特性分支上自由地进行小步、频繁的提交甚至有些“WIP”工作进行中的提交也没关系。本地整理在功能完成、准备推送前使用git rebase -i对本地提交进行整理、压缩形成几个逻辑清晰的提交。更新基准执行git rebase master或main,develop将我的分支变基到主分支的最新状态并在本地解决可能出现的冲突。测试在变基后的分支上运行测试确保一切正常。推送与合并将整理好的、基于最新代码的分支推送到远程创建Pull Request。在PR被审核通过后在GitHub/GitLab界面上选择“Squash and merge”或“Create a merge commit”。如果我的分支提交历史已经非常整洁我会让维护者使用“Rebase and merge”如果平台支持这样主分支历史依然是线性的。如果我的分支提交较多或者想强调这是一个完整的功能单元我会选择“Create a merge commit”生成一个合并提交。虽然这会产生一个分叉点但这个合并提交的信息如PR编号、简述本身也很有价值。这个工作流的核心思想是在本地和个人的分支上使用rebase来保持历史的整洁和可控在集成到共享的主干时根据情况灵活选择合并策略兼顾清晰度和上下文记录。它既享受了rebase带来的清晰又通过最终的合并提交可选为重要的集成点留下了标记。最后记住无论选择哪种方式团队内部达成一致最重要。和你的队友定好规矩然后一起遵守这比单纯追求某种“最佳实践”更重要。工具是为人服务的而不是反过来。希望这篇超详细的图解和实战指南能让你彻底告别对git rebase的恐惧自信地用它来打造更优雅的代码历史。