ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Git Rebase实战:从混乱提交到线性历史

Git Rebase实战:从混乱提交到线性历史 接手一个项目第一次git log --oneline就给我看懵了。提交记录里密密麻麻都是 “Merge branch dev into feature”夹杂着一堆 “update”、“fix”、“aaa” 这样的提交信息根本看不出每条改动到底做了什么。后来我开始认真用git rebase情况才慢慢好转。简单说rebase 就是把一段提交历史“连根拔起”换一块地基重新摆好。听起来很玄但它几乎是每个 git 使用者进阶路上绕不过去的一道坎。这篇文章我不打算从 git 安装开始讲默认你已经有一个能用的 git 环境目标只有一个用大量实际例子把git rebase讲透让正在被 merge 历史困扰的人看完就能在自己的项目里放心用起来。1. 先搞清楚rebase 到底在“变”什么1.1 一个贴近日常的痛点场景我先用大白话把场景画出来。假设你们团队的主分支叫master你已经基于它拉了一个需求分支feature/order-detail正在开发。与此同时同事往master上合入了一个热修复。此时提交历史大概是这个样子master: M1 - M2 - M3 \ feature/order-detail: F1 - F2你的功能分支从 M2 长出来已经提交了 F1、F2而 master 上多了一个 M3。这时候你想让 feature 分支也包含 M3 的修复不然联调的时候还在用旧代码。摆在面前的有两条路git merge master在 feature 分支上产生一个 merge commit把两条线汇到一起。git rebase master把 F1、F2 重新“播放”到 M3 之后形成一条干净的直线。“都是让分支包含最新 master为什么要选 rebase”区别就在提交历史。merge 的特点是保留两条开发线的真实交汇点历史里能看到一个“回”字型分叉而 rebase 的特点是“假装”你的功能分支本来就是在最新 master 上开发的历史是一条没有分叉的直线读起来像流水账一样顺滑。1.2 rebase 的本质把提交摘下来换地基重放很多人觉得 rebase 是个玄学命令其实它做的事有很清晰的步骤。Git 里的每次提交不光保存快照还会记录父提交的哈希这样所有提交串成一条链。rebase 要做的核心工作就是三步找到当前分支和目标分支的“分叉点”也就是共同祖先提交。把当前分支在分叉点之后的提交全部暂时“摘下来”。以目标分支的最新提交作为新的基线把摘下来的提交一个接一个重新应用上去。因为“父提交”变了这些重新应用出来的提交哈希也会跟着变。换句话说在 Git 眼里它们是全新的提交只是内容和作者信息保留了下来。这一点非常关键它直接解释了为什么经常听到一句话不要对已经推到公共仓库且多人合作的分支做 rebase。原因很简单一旦改写和这条分支相关的所有旧哈希都会作废别人本地拉下来的东西就会对不上账。2. 最常见用法把功能分支 rebase 到最新 master 上2.1 先搭一个能复现的实验仓库实操之前我强烈建议你建一个临时仓库来验证。很多人第一次用 rebase 就对着真实项目操作一旦看岔眼很容易把自己吓到。下面这段命令可以在任意空目录里跑一下得到一个完整的实验场景mkdir rebase-demo cd rebase-demo git init # master 上打三个点 echo init a.txt git add . git commit -m M1: init echo module a.txt git add . git commit -m M2: add order module # 从 M2 切出功能分支并提交 git checkout -b feature/order-detail echo api b.txt git add . git commit -m F1: add order detail api echo ui c.txt git add . git commit -m F2: add order detail ui # 回到 master同事合入了热修复 git checkout master echo fix d.txt git add . git commit -m M3: fix login redirect bug跑完以后用git log --oneline --graph --all看一下可以清楚看到 master 和 feature 已经分叉。2.2 执行 rebase 并观察提交链变化切换到 feature 分支再执行git checkout feature/order-detail git rebase master也可以一条命令搞定git rebase master feature/order-detail效果一样。执行之后如果没有冲突git 会直接完成 rebase。再用git log --oneline --graph看历史就变成了F2: add order detail ui F1: add order detail api M3: fix login redirect bug M2: add order module M1: init注意此时 F1、F2 的内容还在但提交哈希已经变了。它们的父提交从 M2 变成了 M3起点已经被“拔”起来挪到了最新 master 之上。2.3 遇到冲突时的标准处理流程rebase 最劝退新手的就是冲突。假设同事在 M3 里也改过b.txt而你的 F1 恰好也改了同一行rebase 就会停下来输出类似这样的提示CONFLICT (content): Merge conflict in b.txt error: could not apply F1... add order detail api hint: Resolve all conflicts manually, mark them as resolved with git add, then run git rebase --continue.看到这个提示不要慌处理顺序非常固定执行git status列出当前冲突文件。打开冲突文件把大小括号标记里的两段内容手动合并成最终版本。对解决好的文件执行git add 文件。执行git rebase --continuegit 会继续应用下一条提交。重复上述步骤直到所有提交都复放完毕。万一 rebase 到一半发现思路乱了果断用git rebase --abort回到开始之前的状态。这里我特别想强调三个命令的分工遇到问题别再瞎试git rebase --continue冲突解决完后继续往下走。git rebase --skip跳过当前这一条提交。慎重因为它会把这个提交的改动整个丢掉。git rebase --abort完全放弃本次 rebase回到命令执行前的状态。我在实际工作中见过不少人在 rebase 冲突时手忙脚乱一边解决冲突一边去改别的代码最后越搞越乱。我的经验是rebase 之前先确保工作区干净冲突一旦发生只处理冲突别顺手改其他东西。3. 交互式 rebase把乱糟糟的提交史整理成给人看的版本3.1 什么时候需要-i模式交互式 rebase也就是git rebase -i是我个人觉得 git 里性价比最高的功能之一。它的典型场景包括一个功能拆了七八个提交提交信息却全是 “update”、“fix typo”、“aaa”想把它们合并成一条语义清晰的记录。想修改历史中某一条提交的信息而不是只改最近一条。想把一个改动巨大、涉及十几个文件的“大泥球”提交拆成几个逻辑独立的提交。想调整本地提交之间的顺序让提交历史更符合代码演进逻辑。这些操作本质上都是“改写历史”但有个安全前提只改写还没有推到远程共享分支的提交。判断标准很简单这条提交只存在于你自己的本地没有别人基于它继续开发过那就可以放心整理。3.2 一个完整的合并提交实例假设你的 feature 分支上有三个提交feat: add order detail page fix typo in order detail page add order detail page again一看就是开发过程中的真实痕迹先写了主体改了个拼写又补了一版文件。这种历史如果直接推上去reviewer 读起来会非常痛苦。我通常会执行git rebase -i HEAD~3编辑器会弹出类似下面的内容pick 8f3a2d1 feat: add order detail page pick 7c2b9e4 fix typo in order detail page pick 2b01c68 add order detail page again我们把第二、三行的pick改成squashpick 8f3a2d1 feat: add order detail page squash 7c2b9e4 fix typo in order detail page squash 2b01c68 add order detail page again保存退出后git 会再打开一个编辑器让你写合并后的提交信息最后这两条提交就被压进了第一条里远程看到的只剩一条feat: add order detail page-i界面里最常用的几个指令我再列一下方便你对照指令作用pick保留这个提交不改动reword保留提交但要修改提交信息edit保留提交但停下来允许修改内容或拆分squash把该提交合并到上一个提交同时合并提交信息fixup把该提交合并到上一个提交且丢弃该提交的提交信息drop删除这条提交如果只是想把第三条“补漏”的提交合并进第一条又不想写两遍提交信息我一般优先用fixup一次搞定不会弹出多余的编辑界面。3.3 修改历史提交信息reword修改历史提交信息有两种常见场景。第一种是最近一条提交的信息写错了用git commit --amend就能解决。第二种是历史中某几条提交信息不清晰想改得规范一点。第二种就要用reword。比如想把 “fix typo in order detail page” 改成 “docs: fix typo in order detail page”在-i界面里把那一行的pick改成reword保存退出后git 会直接进入那次提交的信息编辑界面改完保存即可。这个能力是git commit --amend给不了的因为它只能改最近一条。3.4 把大提交拆成几个edit这个操作适合代码 review 文化比较严格的团队。有一次我帮同事处理提交他一口气把一个“订单详情功能”全写在一个提交里涉及数据模型、接口、页面三个层面大概改了三十多个文件。这种提交很难 review也不利于将来用git bisect定位问题。更合理的方式是拆成三条提交。做法是在-i界面里把大提交的pick改成edit保存退出后 git 会停留在那一次提交的位置上接下来# 回退一次提交把暂存内容重新放回工作区 git reset HEAD^ # 按逻辑分批提交 git add src/model/order.go git commit -m feat: add order model git add src/api/order.go git commit -m feat: add order api git add src/page/order.vue git commit -m feat: add order page # 继续剩下的提交 git rebase --continue第一次用这招的人可能会慌怕把改动弄丢了。其实git reset HEAD^只是撤销了“提交记录”这一层包装所有改动文件都还在工作区里完全没有数据损失。你只需要相信 git 的文件保留机制然后一条一条重新组织。3.5 调整提交顺序让历史更符合逻辑调整顺序在-i界面里最直观直接把对应行上下移动就行。git 会按照你调整后的顺序重新应用提交。要注意的是如果两个提交改的是同一个文件的同一段逻辑调整顺序可能触发冲突。这不是 bug只是 git 要求你确认新顺序下代码依然成立。解决方式和第 2.3 节完全一样。4. rebase 远程分支与推送最刺激也最容易翻车的一块4.1 push 之前先 rebase 整理在团队协作里我养成了一个习惯不管写什么功能在推送合入之前都会做两件事先git rebase master把最新主线拉进自己的分支再git rebase -i把已有提交整理干净最后才git push。这样做的好处非常直接远程仓库上留下的是一条清晰、没有多余 merge commit 的线性历史。reviewer 读代码的时候可以顺着一条线往下走不需要对着分叉图猜“这地方到底是谁并进来的”。4.2 rebase 之后 push 被拒绝怎么办如果分支已经推过远程并且这次 rebase 改写了提交哈希那么再次 push 时大概率会碰到这样的报错To github.com:xxx/rebase-demo.git ! [rejected] feature/order-detail - feature/order-detail (non-fast-forward) error: failed to push some refs to xxx原因很简单远程分支还指向旧的那串提交本地分支已经换成新的哈希Git 认为你的本地落后于远程。如果你非常确定这个分支只有你一个人在用可以用强制推送git push --force-with-lease origin feature/order-detail这里我强烈建议用--force-with-lease而不是直接-f。区别在于--force-with-lease会先检查远程分支是否还是你上次拉取时的状态如果是才强制推送如果别人在你拉取之后又推了新提交它会拒绝执行避免把别人的改动覆盖没了。这个保护机制在多人协作里意义重大比如你和同事同时在两个分支上开发你的本地信息可能已经过期强推就很容易造成事故。IDEA、VS Code 这类 IDE 里一般也有强制推送选项真要用的话优先选“Force Push with Lease”这一类带保护的选项。4.3 铁律永远不要 rebase 公共分支这个道理值得反复强调。公共分支上的提交很可能已经有同事基于它们开发了新的提交。如果你把公共分支 rebase 一遍所有 commit 哈希全部重新生成其他人的本地仓库就会出现“同一批提交变成两堆提交”的混乱严重时看起来像“我的提交消失了”。轻则浪费半天排查重则把别人还没有推完的改动搅乱。我的处理原则非常明确只 rebase 私有的、尚未共享的分支公共分支的合并优先考虑 merge 或三方合并不做任何改写操作。5. rebase 与 merge什么时候该用谁5.1 一张表看清两者的差异下面这张表是我在很多次实践后总结出来的基本上把git merge和git rebase的核心差异都覆盖了对比项git mergegit rebase历史形态保留两条开发线会出现 merge commit线性历史没有分叉节点提交哈希不改变已有提交重新生成目标提交之后所有提交的哈希冲突解决通常在 merge commit 处一次性解决每一条被复放的提交都可能停下来解决冲突安全性相对安全不改写已有历史改写历史误操作影响范围大典型场景合并公共分支、保留发布轨迹本地整理提交、同步最新主线表格能帮你快速记忆但实践感受还得补充两句。merge 的思路是“承认开发是并行的”两个人各改各的最终在一个汇合点合并所以历史是一张网。rebase 的思路是“假装历史是线性的”把自己的改动变成从最新基线自然生长出来的部分。二者没有绝对优劣用错场景才会出问题。5.2 我给团队定的简单规则经历过几次事故后我现在给团队约定的是这样的规则合入master、release这类公共分支时默认用 merge保留合并轨迹方便追溯发布内容。开发自己的feature分支时每次同步主线用 rebase保持分支清爽。提交 MR/PR 之前用git rebase -i把零碎提交合并整理掉。不轻易对别人的提交执行任何改写操作。这套规则既保证了“历史可追溯”又照顾到了“开发体验”在大多数中小规模团队里落地效果都很不错。6. 常见错误与排查速查6.1 rebase 之后发现少了一个提交怎么找回这是很多人在刚接触 rebase 时最害怕的场面。其实 git 提供了后悔药核心工具是git reflog。每次分支变更包括 rebase、merge、resetgit 都会在本地记录一份操作日志。执行git reflog找到 rebase 之前的那一条记录比如HEAD{5}然后把它恢复出来git checkout -b recovery HEAD{5} # 或者想保留原分支只找回某条提交 git cherry-pick 丢失提交的哈希我还有一个个人习惯在重要 rebase 之前先顺手建一个临时备份分支。git branch backup-feature就算后面操作失误一个git checkout backup-feature就能回到原点这个习惯帮我避免过好几次大事故。6.2 碰到 detached HEAD 怎么理解有时候从 rebase 中间状态切走或者误操作了一个 checkout终端会提示 “detached HEAD”。意思是当前不在分支顶端而是直接停在了某个具体提交上。处理方法并不复杂如果只是看代码执行git checkout feature离开即可如果想基于当前提交重新建分支就执行git checkout -b new-branch。6.3 fatal: not a git repository 类报错在子目录里执行 git 命令时偶尔会遇到fatal: not a git repository (or any of the parent directories): .git。这说明当前目录并不在某一个仓库内或者仓库路径已经被移动过。处理方式是检查有没有.git目录cd回仓库根目录再试另外确认 IDE 集成的终端是否真的把工作目录设置到了项目根目录。这类报错多数和 rebase 本身无关更多是环境定位问题。6.4 别把 rebase 和 pull 搞混Git 里有一种操作叫git pull --rebase它等价于先git fetch再git rebase。也就是说拉取远程新提交后自动把自己的本地提交 rebase 到远程最新提交之上这样本地同步的时候就不会产生多余的 merge commit。如果你的团队约定好了普遍采用这种方式也没问题。但在公共分支上我依然建议保持默认的 merge 式 pull减少意外改写历史的风险。最后分享一点个人体会。刚学会 rebase 的时候我一直觉得它是个“危险命令”能不用就不用。后来在真实项目里反复实战才意识到它的价值恰恰在于可控和整洁。我现在的习惯是这样的开发完一个 feature 之前会先跑一遍git rebase -i把提交整理到每个提交都是一个逻辑单元然后再同步最新 master。这样推上去的代码reviewer 能顺着提交记录一条条理解设计思路出了问题用git bisect定位也快得多。如果你刚开始用别急着在重要分支上试水先拿练习仓库跑通第 2 节的例子感受一下提交链和哈希的变化再回到真实项目里用心里会稳很多。
返回列表