ARTICLE DETAIL

资讯详情

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

Git已推送commit如何合并?rebase reword fixup实操指南

Git已推送commit如何合并?rebase reword fixup实操指南 1. 为什么 push 之后还要合并 commit先把场景摆出来你吭哧吭哧写了一下午功能分两次提交到了本地仓库然后git push推到了远程。推完一回头发现这两个 commit 一个写的是fix: 修复登录接口超时另一个写的是fix2: 改了一下超时时间看着就难受。或者更常见的情况是你本地改了 10 次才把需求写完本地历史乱成一锅粥结果因为赶工期直接全部 push 上去了远程仓库的提交记录比你的桌面还乱。这个笔记就是解决这个问题的已经 push 到远程了现在想把最近的两个 commit 合并成一个并且用 reword 和 fixup 这两个操作把提交历史收拾干净。先说清楚一件事很多人一听到“改已 push 的 commit”就慌觉得会搞坏仓库其实没这么玄乎。Git 的历史改写能力本来就是设计好的功能你只要搞清楚其中的原理、知道哪些操作是安全的就能放心用。合并 commit 本质上就是git rebase -i这个交互式命令的一个典型应用场景搭配 reword 和 fixup 两个指令分别负责“改提交信息”和“把当前 commit 的内容并进上一个 commit 并丢弃自身”就能实现把多个提交压成一个、同时修正最终提交信息的效果。这篇笔记适合谁刚用 Git 三个月、还在被各种命令绕晕的新手以及用 Git 三五年、但一直没搞清楚 rebase 交互式界面到底怎么用的老油条。读完你不仅能解决“合并两个已 push 的 commit”这个具体问题还能把 reword、fixup、squash、edit 这几个常见的 rebase 指令彻底弄明白以后遇到类似的提交历史整理需求都不用手忙脚乱去翻文档。先说结论操作不复杂核心就三条命令——git log看提交记录、git rebase -i HEAD~2进入交互式整理、git push --force-with-lease同步远程。但每条命令背后的原理和坑值得展开细细讲一遍。2. 动已推送的 commit 之前先把这五件事想清楚2.1 强推的本质是在“改写历史”如果你只是本地提交没 push随便怎么 rebase 都无所谓因为影响范围只有你一个人。但一旦 commit 已经推送到远程你再对本地提交做变基合并本地的提交 hash 就会变化和远程的提交记录就“分叉”了。这个时候普通的git push会被 Git 拒绝因为 Git 认为你的本地历史落后于远程历史或者至少不是远程历史的线性延伸。你只能用git push --force这类强推命令把远程的提交历史硬生生改成你本地的样子。“改写历史”这四个字往轻了说是让提交记录变得整洁往重了说就是如果别人已经从远程拉取过这些 commit你的强推会直接导致别人的本地仓库和远程不一致严重的时候别人 pull 下来会看到一堆莫名其妙的重复提交甚至冲突。所以操作之前必须确认这些 commit 是你一个人私有的分支上的还是多人共享的开发分支上的。私有分支随便改共享分支务必谨慎。2.2 适合合并 commit 的典型场景我用下来最合适的场景其实是这三种。第一种是功能分支还没合并进主干前你想把开发过程中产生的各种“临时提交”“调试提交”“格式化提交”压成几个有意义的正式提交这样将来 code review 或者回看历史的时候一目了然。第二种是已经合并进主干但还没发布、其他同事还没开始基于这个版本开发你可以抓紧时间修正一下提交信息比如补上需求单号、修正拼写错误。第三种是个人维护的小项目整个仓库只有你一个人提交那你想怎么整理历史都随心所欲。不适合动的场景也很明确主干分支上已经有一堆人基于它开发了你贸然强推改历史轻则让同事产生无谓的合并冲突重则直接把别人的提交覆盖掉这种事故在团队里出一次就够你在周会上做一次深刻检讨了。2.3 改历史之前先留一条后路我在实际操作中养成了一个习惯做任何 rebase 之前先把当前分支的状态备份成一个临时分支。命令很简单git branch backup/my-branch-before-rebase这行命令就是在当前 commit 上建一个分支指针如果后面操作搞砸了直接git reset --hard backup/my-branch-before-rebase就能回到操作前的状态比在 rebase 过程中抢救要省心得多。另外用git rebase的时候Git 会有一个 reflog 机制你在当前分支上的每一次 HEAD 变动都会被记录在.git/logs/HEAD这个文件里。即使你 rebase 到一半觉得不对用git reflog也能找到变基前的 commit hash然后切回去。不过对新手来说git reflog看起来有点晦涩不如备份分支来得直观。所以别嫌多此一举备份分支真的能救命。2.4 明确 reword、fixup 和几个相近指令的边界很多人在网上查资料看到 reword、squash、fixup 这几个词放在一起就开始晕。我先把它们几个的区别理清楚reword保留当前 commit 的内容和变更只修改这条 commit 的提交信息。好比同一本书换了个封面。squash把当前 commit 合并到上一个 commit并把当前 commit 的提交信息保留下来供你编辑最终你可以把两条提交信息合成一条。好比两杯水倒进一个杯子里两杯水的标签都还在你要决定最终贴哪个。fixup把当前 commit 合并到上一个 commit但直接丢弃当前 commit 的提交信息只保留上一个 commit 的信息。同样是两杯水倒一起fixup 的做法是撕掉第二杯的标签只保留第一杯的标签。edit停下来让你修改这个 commit 的更多细节比如改文件内容、拆分成多个 commit不只是改提交信息。你注意看标题里的组合reword 配合 fixup。为什么不是只用 squash因为 squash 在合并时会打开编辑器让你重新编辑提交信息而 fixup 完全使用上一个 commit 的信息。如果我想保留第一个 commit 的提交信息不变直接用 fixup 就可以了连编辑器都不用进。但如果我想要最终合并后的 commit 采用一个全新的信息那就得先 fixup 把第二个 commit 的内容并进去然后再 reword 修改第一个 commit 的信息。2.5 一个核心概念为什么 rebase 会改变 commit hash要理解合并 commit 为什么需要强推你得先弄明白 commit hash 是怎么生成的。Git 里每个 commit 的 hash 不只是根据这次改了什么内容算出来的而是根据一系列元数据算出来的其中就包括父 commit 的 hash、作者信息、提交时间、提交信息、代码内容快照。所以只要父 commit 变了自己就会跟着变提交信息改了hash 也会变就算时间和内容一样只要父引用不同最终 hash 就不同。rebase 的本质是把一系列 commit 在一个新的父节点上重新“打地基”然后一个接一个重新生成。所以哪怕你只改了其中一个 commit 的提交信息这个 commit 之后的每一个 commit 的 hash 都会连锁改变。这就是为什么合并 commit 后本地历史会和远程历史“长得完全不一样”必须强推才能对齐。3. 合并两个已推送 commit 的完整实操过程3.1 先查看提交记录确定操作范围打开终端进入你的项目目录先跑一条git log看提交历史。我习惯用git log --oneline每条提交只显示一行输出类似这样a1f2e3d (HEAD - feature/login) fix: 调整登录超时参数 b4c5d6e feat: 实现登录接口 f7a8b9c chore: 初始化项目结构假设我想把最新的两个 commita1f2e3d和b4c5d6e合并成一个那我的操作目标就是这两条。请注意这里的HEAD指向当前最新提交我要操作的其实就是HEAD和它的上一个提交HEAD~1用git rebase -i HEAD~2可以一次把这两个提交放到编辑列表里。在开始之前我再确认一下这两个提交是否已经 push 到远程了。可以用git status查看分支领先远程几个提交更直接的办法是用git log origin/分支名 --oneline对比两边记录。我开发中常用的是git log --oneline --decorate在提交记录后面直接显示origin/feature/login等远程引用指向哪里一眼就能看出来哪些 commit 是已经推过的。这一条确认非常关键。如果你把没 push 的 commit 和已 push 的 commit 混在一起 rebase涉及范围会扩大冲突风险也更高。所以先明确我只动最近两个已 push 的 commit其他的一概不碰。3.2 创建备份分支给自己留退路在执行任何正事之前先跑一条保险命令git branch backup/feature-login-before-rebase这条命令不会切换分支只是把当前 commit 位置记录到一个新分支上。等操作全部完成确认没问题之后你可以把备份分支删掉git branch -D backup/feature-login-before-rebase注意这里用的是大写-D因为备份分支没有被合并到当前分支普通的小写-d会拒绝删除。其实这也是一种保护机制Git 担心你误删了包含未合并提交的分支。3.3 进入交互式变基界面接下来执行核心命令git rebase -i HEAD~2命令中的HEAD~2表示要操作的提交范围是 HEAD 之前包含 HEAD的 2 个提交。Git 会打开一个文本编辑器默认一般是 vim内容类似这样pick b4c5d6e feat: 实现登录接口 pick a1f2e3d fix: 调整登录超时参数 # Rebase f7a8b9c..a1f2e3d onto f7a8b9c (2 commands) # # Commands: # p, pick use commit # r, reword use commit, but edit the commit message # e, edit use commit, but stop for amending # s, squash use commit, but meld into previous commit # f, fixup use commit, but discard commit message and meld into previous commit # x, exec run command (the rest of the line) using shell # d, drop remove commit注意看这列表是“从旧到新”排序的最上面是最老的那一条也就是b4c5d6e然后才是更新的a1f2e3d。这个顺序和git log正好相反初学者经常在这里栽跟头把新老顺序搞反了。我要把这两个 commit 合并成一个并且把最终提交信息改成一条更简洁的说明。按照标题里的思路用 reword 和 fixup操作步骤是第一条较老的b4c5d6e前缀pick改为reword表示这条 commit 的内容保留但我回头要重新编辑它的提交信息。第二条较新的a1f2e3d前缀改为fixup表示这条 commit 的内容直接合并进上一条并且它的提交信息直接丢弃。修改完后的列表长这样reword b4c5d6e feat: 实现登录接口 fixup a1f2e3d fix: 调整登录超时参数保存并退出编辑器。如果你用的是 vim按Esc然后输入:wq回车。3.4 填入最终提交信息退出第一个编辑器之后因为上面第一条标记了 rewordGit 马上会再次打开编辑器让你重新填写这条提交的提交信息。此时你可以把原来那句feat: 实现登录接口改成更能概括这次合并结果的话比如feat: 实现登录接口并调整超时参数保存退出。到此为止rebase 的主体工作已经完成。刚才那一对提交已经合并成了一个新 commit这个新 commit 的 hash 和原来两个都不相同。我们来验证一下效果git log --oneline输出应该是x9y8z7a (HEAD - feature/login) feat: 实现登录接口并调整超时参数 f7a8b9c chore: 初始化项目结构原来最新的两个提交已经合体成一个提交信息也被修正了。本地的操作到此结束接下来是同步远程。3.5 同步到远程的两种强推方式这一步是很多新手最纠结的地方。直接执行git push大概率会遇到这样一段报错! [rejected] feature/login - feature/login (non-fast-forward) error: failed to push some refs to gitgithub.com:xxx/project.git hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart. If you want to integrate the remote changes, hint: use git pull before pushing again.这个报错完全符合预期因为远程的提交历史和本地已经分叉了。你本地已经没有原来那两个 commit 了取而代之的是一个新的合并后的 commit所以远程认为你“丢失”了两个提交拒绝接受你的推送。正确的做法是强制推送。Git 提供了两种写法git push --force git push --force-with-lease我强烈建议你用第二种--force-with-lease而不是第一种--force。原因很简单--force是无条件的它不管远程分支现在是什么状态直接把远程历史改成你本地的样子。万一在你 rebase 和强推之间有同事往这个分支推了新代码你这一下会把同事的提交直接抹掉。--force-with-lease则是带了一个“预期值校验”它会检查远程分支是否还是你刚才看到的样子。如果远程在你操作期间被别人推了新提交它就拒绝强推让你重新 pull 整合后再推。说白了--force是拿着推土机往前冲--force-with-lease是推土机前面装了一个感应器发现障碍物会自动刹车。日常开发中除非你知道这个分支绝对无人使用否则一律用带 lease 的版本。3.6 如果需要保留未推送的提交把 reword 换成 edit有人可能会问如果我想合并的两个 commit 并不是最顶部的两条中间还隔着一条没推送的新提交怎么办这种场景其实也很常见比如你这条分支上之前有一个提交已经 push 了之后你又写了两个新提交还没推现在你想把已经 push 的那两个旧提交合并一下但不想碰最新的未推送提交。这种情况下git rebase -i HEAD~3把最新三条提交拉进编辑列表然后把对应的旧提交行改成 reword 和 fixup最后一条未推送的新提交保持 pick 不动。这样 rebase 完未推送的新提交也会被“重放”到新的历史之上hash 会变但内容不变。你强推之后远程拿到的是合并后的旧提交最新提交效果完全符合预期。4. 深入理解 reword 和 fixup 的底层逻辑4.1 为什么 fixup 比 squash 更适合“丢弃信息”从前面的对比表格可以看出来fixup 和 squash 的功能非常接近都是把当前 commit 的内容合并进上一个 commit。唯一的区别在于合并后提交信息的处理方式squash 会把你带进编辑器列出所有参与合并的提交信息让你重新编排fixup 则直接用上一个 commit 的信息连编辑机会都不给你。大多数人推荐 fixup 的原因是它在非交互式操作中特别好用。比如你可以这样写git commit --fixupa1f2e3d这条命令会创建一个特殊的临时提交提交信息会自动以fixup! a1f2e3d开头。然后你再执行git rebase -i --autosquash HEAD~5Git 会自动识别以fixup!开头的提交把对应的提交行自动调整到目标提交的下方并标记为 fixup完全不需要手动编辑。这个组合操作在处理“修改某个提交中的个别文件”这个场景时非常高效可以说是 Git 里隐藏的效率神器。4.2 reword 在交互界面里的执行顺序你注意观察在git rebase -i的编辑器中所有命令的执行顺序是严格的从旧到新逐条应用。标记了 reword 的那一行Git 会在处理到这一步时暂停打开编辑器让你改提交信息标记了 fixup 的那一行Git 会安静地把这个 commit 的内容合并进前一个 commit然后继续往下走。按照我前面的配置reword b4c5d6e feat: 实现登录接口 fixup a1f2e3d fix: 调整登录超时参数整个 rebase 实际发生的事情是先基于f7a8b9c这个老提交重新生成一条新提交内容是原来b4c5d6e的内容但提交信息等你重新输入然后立刻把a1f2e3d的内容合并进这条新提交并丢弃它自己的提交信息。如果你在 reword 步骤填入了新的提交信息最终合并出来的这个新 commit 就采用你填的新信息。如果你懒得用 reword其实还有一个更快捷的变通方案把第一行改成pick第二行改成fixupGit 会直接使用b4c5d6e的原始提交信息你连第二个编辑器窗口都不用打开。但这样一来最终提交信息就固定成了feat: 实现登录接口没办法体现这次合并把“调整超时参数”也包含进去了。所以标题里使用 reword fixup 的组合就是为了在合并的同时把最终提交信息改成更贴切的一句话。我个人的习惯是如果两者信息差异不大直接用 pick fixup 最省事如果合并后想重新提炼一句更完整的信息就 reword fixup。4.3 合并 commit 之后改动行记录会发生什么这是很多人忽略的一个细节。Git 对每次提交的记录不只是提交信息还有这个提交相对于它父提交的“差异”diff。当你把两个 commit 合并成一个新的提交和它父提交之间的 diff是两个原始提交 diff 的累加结果。从代码变更的维度看最终工作区内容是一样的但变更历史被打包成了一份。这意味着如果第一个 commit 改了文件 A第二个 commit 又改了文件 A 的同一处地方那么在合并后的单条提交里Git 不会保存“先改成 X、再改成 Y”的过程只会保存“最终是 Y”的结果。所以在 rebase 过程中如果两个 commit 改动了同一行代码前面的提交要合并到后面时Git 需要做三路合并有可能产生冲突。5. 实操中一定会遇到的四个问题与排查方法5.1 rebase 过程中出现冲突怎么办两个 commit 虽然都是你自己写的但一样有可能在合并时产生冲突。最常见的场景是第一个 commit 新增了一个函数第二个 commit 又修改了同一个函数的同一行。当你把第二个提交 fixup 到第一个提交时Git 要判断到底以哪一版为准。出现冲突时rebase 会停下来终端会提示类似于error: could not apply a1f2e3d... fix: 调整登录超时参数 hint: Resolve all conflicts manually, mark them as resolved with hint: git add/rm conflicted_files, then run git rebase --continue.这时你可以用git status查看哪些文件处于冲突状态打开这些文件你会看到类似这样的冲突标记 HEAD function login(timeout 30) { ... } function login(timeout 60) { ... } a1f2e3d (fix: 调整登录超时参数)处理思路很简单和之间是当前提交第一个 commit的内容和之间是被合并进来第二个 commit的内容。你根据业务需要保留其中一份或者手动改成一份新的内容然后把冲突标记行删除。保存文件后执行git add 冲突文件 git rebase --continue如果你在 rebase 过程中发现怎么都不对劲想回到操作之前的状态可以执行git rebase --abort这个命令会把分支完全恢复到 rebase 开始之前的状态配合前面创建的备份分支双保险。5.2 强推被拒绝报错提示 remote has unexpected content使用--force-with-lease的好处在这里就体现出来了。如果在你 rebase 的期间别人向同一个远程分支推送了新提交--force-with-lease会给出类似这样的提示! [rejected] feature/login - feature/login (stale info) error: failed to push some refs to gitgithub.com:xxx/project.git hint: Updates were rejected because the remote contains work that you do hint: not have locally.这个报错的意思是你提交时的“期望值”和远程的实际状态对不上了。这时千万不要不管三七二十一换成--force硬推正确的做法是先停下来执行git fetch拉取远程最新的引用再对比一下远程分支多出来的提交跟同事确认这些新提交是否可以和你正在整理的历史共存。如果确认同事的提交不应该被覆盖你就要调整策略比如把同事最新的提交先合并到你的分支头部再重新整理你自己的提交。整理完再强推。5.3 合并操作完成后发现改动丢了怎么办有一些极端情况比如在 rebase 过程中误操作删除了某一行代码最后 commit 也提交了push 也推了才发现代码不对劲。这时候不要慌用 reflog 找回来。git reflog输出会列出当前分支每次 HEAD 移动的历史每一行左边是 hash 值右边是操作描述。找到 rebase 操作之前的那个 hash执行git reset --hard 那个hash就能回到 rebase 之前的状态。这个操作会把你刚才合并的成果丢进“未引用”的区域但不会立刻被 Git 垃圾回收。如果你在恢复后发现还有代码散落在刚才的合并提交里可以用 cherry-pick 或者手动复制的方式找回来。虽然有些繁琐但至少数据不会凭空蒸发。5.4 分享几个我踩过的真实坑第一个坑忘记--force-with-lease直接用了--force结果把一个同事刚推上去的热修复覆盖了幸好那个同事留了本地备份花了一下午才恢复现场。从那以后我在所有脚本和文档里统一改用--force-with-lease并且把这条规则写进了团队规范。第二个坑rebase 顺序看反了。第一次操作时我盯着git log的显示顺序把交互界面里的新提交误当成旧提交来处理直接把标签反了合并完之后提交信息完全错乱只能 abort 重来。改历史的操作一定要先把屏幕上的列表顺序搞清楚。第三个坑合并 commit 后没有及时更新远程分支的本地追踪引用导致下一次git status显示的分支领先/落后状态非常混乱。后来我养成了一个习惯强推成功之后立刻跑一条git fetch --prune把远程引用同步一下避免后续误判。6. 把这些命令串成一套实用的工作流程写到最后我把整篇的核心拆成一套可以直接抄的工作流程每次需要合并已推送的 commit 时照着走一遍就行。git log --oneline --decorate确认要合并的提交范围并确认它们已推送到远程。git branch backup/分支名-before-rebase创建备份分支。git rebase -i HEAD~N进入交互式变基N 按需要调整。在编辑器中把较老的提交前缀改为reword较新的提交前缀改为fixup保存退出。在随后打开的编辑器中填写合并后的最终提交信息保存退出。git log --oneline检查合并结果是否符合预期。git push --force-with-lease同步到远程。核对远程分支状态确认没问题后删除备份分支。这套流程从第一次手动执行到最后形成肌肉记忆大概需要两三次实际操作。等你熟悉了 reword 和 fixup 的组合用法还可以进一步探索--autosquash和git commit --fixup的组合用 git 的自动编排把整个 rebase 流程进一步压缩成一条命令。不过那是另外一个阶段的事了先把基础流程跑通你就能在提交历史的整理上游刃有余了。
返回列表