我亲手 git 推送后,生产挂了:一次主干抹除中间提交的复盘

我亲手 git 推送后,生产挂了:一次主干抹除中间提交的复盘

0. 故事的开始

周二下午两点,我 git push 完,去倒了杯水。

回来时,IM 弹窗已经响了一阵 ——

运营:” 线上有个鉴权模块挂了,所有第三方调用 401。”
测试:” 提单平台接口全部不通。”
架构师:”@我 刚那个提交是不是你推的?”

我打开 git log,立刻意识到问题所在:

1
2
3
4
8a15193  (A) feat(加盟商管理):新增加盟商自动充值配置功能
bb1fda9 (B) feat: 新增第三方接入 AK/SK 鉴权功能模块 ← 就是这个
5ca8cea (C) feat(clue):新增线索合并入公海统一入口...
af53969 (D) fix(clue):修复线索回收时错误更新 info_updateTime 的问题

那个 bb1fda9—— 昨晚赶工的” 临时方案”,我只跑通了自己写的那部分用例,Sa-Token 角色适配那一块根本没验证,连带着所有基于系统角色的第三方调用全部异常。

更要命的是:这个提交已经推上 main,而且后面还有两次业务提交 (5ca8ceaaf53969),同事已经拉下去用了。

接下来的一段时间,我们做了一件团队从来没做过的事:从主干抹掉一个” 中间” 提交


1. 任务拆解

需求其实很清楚:

  • bb1fda9 必须从 main 历史中彻底消失(不是 revert,因为新模块不该存在)
  • 5ca8ceaaf53969 必须保留 —— 队友已经基于它们在开发
  • 操作必须可回滚 —— 任何一步出错,一次小事故就会持续放大

关键前提:5ca8ceaaf53969 都没碰 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
2
git checkout main
git checkout -b archive/main-with-bb1fda9

这一步的意图:把当前的 main 原封不动迁到一个备份分支。后续任何一步误操作,archive/main-with-bb1fda9 永远保留着含 bb1fda9 的完整历史,随时 git checkout archive/main-with-bb1fda9 就能回到现场。

4. 第二步:把 main 重置到目标祖先

回到 mainreset --hard8a15193—— 也就是 bb1fda9 之前的最后一个提交:

1
2
git checkout main
git reset --hard 8a15193386adaac44b6b6774e64dd9d07c2c56ab

执行后 main 历史变成:

1
8a15193  (A) feat(加盟商管理):新增加盟商自动充值配置功能

bb1fda95ca8ceaaf53969 三次提交从 main 视角全部消失

写到这里手心都是汗 —— 但心里有底,因为 archive/main-with-bb1fda9 还稳稳地指着 af53969

5. 第三步:用 cherry-pick 把” 好的” 提交重建回去

按时间顺序,逐个 cherry-pick 回 main:

1
2
git cherry-pick 5ca8ceab6b434f676260f3b0d70caa8a35c250c8
git cherry-pick af53969ab6e7cab9b343754100a76b3ba024f700

因为前面已经验证过这两个提交都没动 bb1fda9 引入的文件,落盘非常干净:

1
2
3
4
[main b174dca] feat(clue):新增线索合并入公海统一入口...
6 files changed, 325 insertions(+), 18 deletions(-)
[main 725fd60] fix(clue):修复线索回收时错误更新 info_updateTime 的问题
1 file changed, 3 insertions(+), 2 deletions(-)

注意:cherry-pick 会生成新的 commit hash(这里是 b174dca725fd60),旧 hash 仍可在备份分支 archive/main-with-bb1fda9 上找到 —— 审计和回滚都没问题。

6. 第四步:验证

群里喊了一声” 完成”,但没急着推,先在自己的终端里跑一遍:

1
2
3
4
5
6
7
8
9
git log --oneline -5
# 725fd60 fix(clue):修复线索回收时错误更新 info_updateTime 的问题
# b174dca feat(clue):新增线索合并入公海统一入口...
# 8a15193 feat(加盟商管理):新增加盟商自动充值配置功能
# f55a8c0 feat(franchisee):新增自动充推广币开关功能

git status
# On branch main
# nothing to commit, working tree clean

再确认 bb1fda9 引入的目录和文件确实已被抹除:

1
2
ls jipin-cloud/jipin-biz/src/main/java/top/jipinyun/biz/thirdparty
# DirectoryNotFound

心里一块石头落地。

7. 第五步:推送到远端 —— 但要” 温柔的强推”

这一步是真正的危险动作。由于我们改写了 main 的历史(cherry-pick 产生新 hash),远端 main 与本地分叉,必须使用 --force-with-lease

1
2
3
4
5
# 先 fetch 一次确认远端 main 仍是我们预期的 af53969
git fetch origin
git rev-parse origin/main # 应为 af53969

git push --force-with-lease origin main

--force-with-lease--force 安全 —— 如果在你操作期间有同事推了新提交,git 会拒绝推送,避免误覆盖别人的工作。

最后把备份分支也推上去,方便事后审计与回查:

1
2
git push origin archive/main-with-bb1fda9
# * [new branch] archive/main-with-bb1fda9 -> archive/main-with-bb1fda9

推送完成后 git fetch 一下,看到远端 main 已经是 725fd60,群里发个” 已恢复”,事故结束。


8. 为什么不用 rebase -i drop

理论上 git rebase -i 8a15193^ 然后把 bb1fda9 那一行改成 drop,是最干净的做法。但在 Windows + PowerShell 环境下经常遇到两个坑:

  1. GIT_SEQUENCE_EDITOR 走 bash 风格变量赋值 VAR=val cmd 在 PowerShell 中不识别,必须用 $env:VAR = ...
  2. Git 在 Windows 上生成的 todo 文件 git-rebase-todo 会带 UTF-8 BOM,正则匹配时容易踩坑。

我这次尝试过两轮,第一轮脚本说” 成功”,但 bb1fda9 还稳稳地挂在历史里 —— 因为自定义 PowerShell 脚本因 BOM 没匹配上 pick <hash> 行,drop 没生效,rebase “看似成功其实啥都没做”。非常隐蔽。

结论:当 rebase 自动编辑不可靠、或者想让每一步都可显式验证时,备份分支 + reset + cherry-pick 三连是更稳的选择。生产事故现场,别让隐藏的坑偷走你的时间。


9. 事后复盘清单

  1. 重写 main 是高危操作 —— 必须确认没有其他同事正在 main 上开发;提前在群里通气。
  2. cherry-pick 不保留原始 hash—— 若团队按 hash 关联 PR / CI 工单,需要同步更新对应记录。
  3. 备份分支不要立即删除 —— 建议保留至少一个迭代周期,确认无问题后再 git branch -Dgit push origin --delete
  4. --force-with-lease 而不是 --force—— 多一层保护,能救命。
  5. CI/CD 一定要有” 问题提交” 自动识别 —— 这次如果上线前有契约测试或接口冒烟,能提前 5 分钟发现。

10. 小结

这次实操的本质,是把” 删除中间提交” 这件事拆成三步:

1
归档 → 重置 → 重建

每一步都显式、可验证、可回滚。这种思路在团队协作中尤其有用 —— 既能快速修复历史,又不会因为 rebase 的隐式行为留下隐患。

如果提交之间存在依赖(例如 C 修改了 B 新增的文件),cherry-pick 就会冲突,此时改用 rebase -i drop 或者干脆保留 B 再用 revert 撤销,可能是更优解。

最后一点心得:真正吃时间的,从来不是事故本身,而是事后未经深思的二次操作。 先备份,再动手,永远比” 再快一点” 更值得。