我亲手 git 推送后,生产挂了:一次主干抹除中间提交的复盘
我亲手 git 推送后,生产挂了:一次主干抹除中间提交的复盘
0. 故事的开始
周二下午两点,我 git push 完,去倒了杯水。
回来时,IM 弹窗已经响了一阵 ——
运营:” 线上有个鉴权模块挂了,所有第三方调用 401。”
测试:” 提单平台接口全部不通。”
架构师:”@我 刚那个提交是不是你推的?”
我打开 git log,立刻意识到问题所在:
1 | 8a15193 (A) feat(加盟商管理):新增加盟商自动充值配置功能 |
那个 bb1fda9—— 昨晚赶工的” 临时方案”,我只跑通了自己写的那部分用例,Sa-Token 角色适配那一块根本没验证,连带着所有基于系统角色的第三方调用全部异常。
更要命的是:这个提交已经推上 main,而且后面还有两次业务提交 (5ca8cea、af53969),同事已经拉下去用了。
接下来的一段时间,我们做了一件团队从来没做过的事:从主干抹掉一个” 中间” 提交。
1. 任务拆解
需求其实很清楚:
bb1fda9必须从main历史中彻底消失(不是revert,因为新模块不该存在)5ca8cea、af53969必须保留 —— 队友已经基于它们在开发- 操作必须可回滚 —— 任何一步出错,一次小事故就会持续放大
关键前提:
5ca8cea和af53969都没碰bb1fda9引入的文件,所以可以干净回放。
2. 方案对比
| 方案 | 优点 | 缺点 | 当时选了吗 |
|---|---|---|---|
rebase -i drop |
历史最干净,单条直线 | Windows 下 GIT_SEQUENCE_EDITOR 走 PowerShell 经常踩 BOM 编码坑;脚本匹配失败时” 看似成功其实没动” |
否 |
git revert |
安全、可审计 | 会留下一个 Revert "..." 提交;模块代码仍残留在对象库里 |
否 |
| 新建备份分支 + 重置 main + cherry-pick | 全程显式、可验证、可回滚、零编码坑 | 多两步操作,cherry-pick 产生新 hash | 是 |
我们选了第三种 —— 架构师敲定的,理由也很简单:每一步都能 git status 验证,踩坑能立刻停。
3. 第一步:先把 main 整体” 搬出去”
直觉上,” 删提交” 应该先 reset,但正确顺序是先备份 —— 这是这次没翻车的关键。
1 | git checkout main |
这一步的意图:把当前的 main 原封不动迁到一个备份分支。后续任何一步误操作,
archive/main-with-bb1fda9永远保留着含bb1fda9的完整历史,随时git checkout archive/main-with-bb1fda9就能回到现场。
4. 第二步:把 main 重置到目标祖先
回到 main 并 reset --hard 到 8a15193—— 也就是 bb1fda9 之前的最后一个提交:
1 | git checkout main |
执行后 main 历史变成:
1 | 8a15193 (A) feat(加盟商管理):新增加盟商自动充值配置功能 |
bb1fda9、5ca8cea、af53969 三次提交从 main 视角全部消失。
写到这里手心都是汗 —— 但心里有底,因为
archive/main-with-bb1fda9还稳稳地指着af53969。
5. 第三步:用 cherry-pick 把” 好的” 提交重建回去
按时间顺序,逐个 cherry-pick 回 main:
1 | git cherry-pick 5ca8ceab6b434f676260f3b0d70caa8a35c250c8 |
因为前面已经验证过这两个提交都没动 bb1fda9 引入的文件,落盘非常干净:
1 | [main b174dca] feat(clue):新增线索合并入公海统一入口... |
注意:cherry-pick 会生成新的 commit hash(这里是
b174dca、725fd60),旧 hash 仍可在备份分支archive/main-with-bb1fda9上找到 —— 审计和回滚都没问题。
6. 第四步:验证
群里喊了一声” 完成”,但没急着推,先在自己的终端里跑一遍:
1 | git log --oneline -5 |
再确认 bb1fda9 引入的目录和文件确实已被抹除:
1 | ls jipin-cloud/jipin-biz/src/main/java/top/jipinyun/biz/thirdparty |
心里一块石头落地。
7. 第五步:推送到远端 —— 但要” 温柔的强推”
这一步是真正的危险动作。由于我们改写了 main 的历史(cherry-pick 产生新 hash),远端 main 与本地分叉,必须使用 --force-with-lease:
1 | # 先 fetch 一次确认远端 main 仍是我们预期的 af53969 |
--force-with-lease 比 --force 安全 —— 如果在你操作期间有同事推了新提交,git 会拒绝推送,避免误覆盖别人的工作。
最后把备份分支也推上去,方便事后审计与回查:
1 | git push origin archive/main-with-bb1fda9 |
推送完成后 git fetch 一下,看到远端 main 已经是 725fd60,群里发个” 已恢复”,事故结束。
8. 为什么不用 rebase -i drop
理论上 git rebase -i 8a15193^ 然后把 bb1fda9 那一行改成 drop,是最干净的做法。但在 Windows + PowerShell 环境下经常遇到两个坑:
GIT_SEQUENCE_EDITOR走 bash 风格变量赋值VAR=val cmd在 PowerShell 中不识别,必须用$env:VAR = ...。- Git 在 Windows 上生成的 todo 文件
git-rebase-todo会带 UTF-8 BOM,正则匹配时容易踩坑。
我这次尝试过两轮,第一轮脚本说” 成功”,但 bb1fda9 还稳稳地挂在历史里 —— 因为自定义 PowerShell 脚本因 BOM 没匹配上 pick <hash> 行,drop 没生效,rebase “看似成功其实啥都没做”。非常隐蔽。
结论:当 rebase 自动编辑不可靠、或者想让每一步都可显式验证时,备份分支 + reset + cherry-pick 三连是更稳的选择。生产事故现场,别让隐藏的坑偷走你的时间。
9. 事后复盘清单
- 重写 main 是高危操作 —— 必须确认没有其他同事正在 main 上开发;提前在群里通气。
- cherry-pick 不保留原始 hash—— 若团队按 hash 关联 PR / CI 工单,需要同步更新对应记录。
- 备份分支不要立即删除 —— 建议保留至少一个迭代周期,确认无问题后再
git branch -D或git push origin --delete。 --force-with-lease而不是--force—— 多一层保护,能救命。- CI/CD 一定要有” 问题提交” 自动识别 —— 这次如果上线前有契约测试或接口冒烟,能提前 5 分钟发现。
10. 小结
这次实操的本质,是把” 删除中间提交” 这件事拆成三步:
1 | 归档 → 重置 → 重建 |
每一步都显式、可验证、可回滚。这种思路在团队协作中尤其有用 —— 既能快速修复历史,又不会因为 rebase 的隐式行为留下隐患。
如果提交之间存在依赖(例如 C 修改了 B 新增的文件),cherry-pick 就会冲突,此时改用
rebase -i drop或者干脆保留 B 再用revert撤销,可能是更优解。
最后一点心得:真正吃时间的,从来不是事故本身,而是事后未经深思的二次操作。 先备份,再动手,永远比” 再快一点” 更值得。