分支事故复盘:基线分支选错,以及如何用 Cherry-pick 补救 / Branch Incident Review and Recovery with Cherry-Pick
1. 这次事故是什么
这次我们在推进 MapDocument 文档化重构时,后面才发现当前工作分支并不是基于预期的 main 最新 UI 基线,而是从另一个已经偏离的分支演化过来的。
这带来了一个非常典型但也非常危险的问题:
- 我们以为自己是在“重构后端存储结构”
- 实际上工作区同时混入了“旧 UI 状态”和“新数据结构改造”
- 最终出现的现象不是单一 bug,而是“基线不一致导致的系统性错位”
最直接的表现就是:
- 前端 UI 和
main上记忆中的版本不一致 - 一些之前存在的交互能力消失了,比如侧边栏收缩、Graph 缩放/拖拽、Profile 入口等
- 很难一眼判断“到底是这次重构改坏了”,还是“本来这个分支就不是正确的 UI 基线”
所以这不是普通的小冲突,而是一次典型的“分支基线事故”。
2. 根因是什么
根因并不复杂,但后果很大:
根因 1:起始分支不是正确基线
这次重构最开始所在的分支,不是从真正想要合入的 main 最新状态切出来的,而是从另一个存在 UI 偏差的分支继续开发。
这意味着:
- 数据结构重构本身是对的
- 但是承载它的前端基线是错的
- 于是越往后做,越容易把“基线差异”误判成“本次改动引入的回归”
根因 2:改动跨度很大,难以整体搬运
这次改动不是单点修复,而是跨越多层:
- Prisma schema
- 后端 repository/service
- shared contracts
- frontend map/progress 数据流
- 本地种子数据
- 若干 UI 恢复补丁
如果直接整分支合并,很容易把错误 UI 基线也一起带进来。
根因 3:冲突不是语法冲突,而是语义冲突
真正麻烦的不是“文件冲突了”,而是“两个分支分别代表两种不同阶段的系统状态”:
- 一边是正确方向的数据结构重构
- 一边是更接近目标的
mainUI 基线
所以不能机械地 merge,必须有意识地筛选“哪些提交值得搬运,哪些状态不该被带过去”。
3. 为什么当时不能直接 merge
如果直接把旧工作分支 merge 到新的 main 基线分支里,会有几个风险:
- 把旧分支里不想要的 UI 状态也带过来
- 很难保证历史整洁
- 容易把本来局部的补丁放大成全局冲突
- 后面很难解释这次 PR 到底真正改了什么
更关键的是,直接 merge 以后,提交历史里会混入大量“和目标无关的基线差异”。这样哪怕最后能跑,也会让后续排查变得非常痛苦。
4. 为什么选择 Cherry-pick
我们最后采用的核心补救策略是:
- 回到真正的
main基线 - 新建一个干净分支
- 把上午在错误基线分支上做对的那些提交,一个个“摘”过来
- 在新的基线上解决冲突和 UI 回归
这就是 git cherry-pick 最适合的场景:
- 你不想整条分支一起搬过来
- 你只想保留其中一部分“有价值的提交”
- 你希望最终的新分支建立在正确基线上
换句话说,cherry-pick 不是为了炫技,而是为了把“正确的改动”从“错误的基线”里救出来。
5. 我们是怎么做的
第一步:切回 main,建立真正的目标分支
先回到 main,然后从 main 新建这次真正要交付的分支:
git switch main
git switch -c map-document-refactor-mainbase这一步的意义非常大:
- 从这里开始,新的工作只建立在
main之上 - 后面任何冲突,都是“重构内容 vs main 基线”的冲突
- 不再混入之前错误分支的整体状态
第二步:梳理原分支里真正想保留的提交
我们并不是把整个旧分支照搬,而是挑出这次真正想保留的提交。
这次最终搬运过来的提交链大致如下:
1000d84 docs: add map storage refactor design spec
f2c16b8 feat: define document-centered map contracts
efd7342 refactor: move prisma schema to map documents
b3933fd feat: add map document validation and projection scripts
da070cf refactor: move backend services to document-based maps
256e666 refactor: consume document-based maps in frontend
34c1d33 fix: align progress validation with document payload
47fba02 chore: remove legacy file-based map artifacts
f3188ff refactor: remove legacy map compatibility types
81d2784 refactor: remove legacy node api route
b5b21ac docs: document map authoring workflow
1639102 fix: fallback to canonical map document when db is empty
0efbbfb fix: seed a usable default map document for local development
c860763 fix: restore profile entry in the main header
bea34b3 fix: stabilize main-based document map branch
3810458 fix: restore local auth and cors flow
89a35b8 fix: resolve prisma schema merge remnants
7f9f807 fix: restore mainbase graph interactions and sidebar
ed33aba fix: restore graph dragging and edge visibility这些提交在新的 main 基线上,最终形成了我们现在这条可提 PR 的分支历史。
第三步:按顺序执行 cherry-pick
cherry-pick 的关键原则是:尽量按原本的逻辑顺序来摘取提交。
例如:
git cherry-pick 1000d84 f2c16b8 efd7342 b3933fd da070cf 256e666再逐步继续摘后面的修复提交。
为什么强调“顺序”:
- contract 先到,后端和前端才能接
- schema 先到,repository/service 才有落点
- 数据流稳定后,UI 修复才有意义
如果顺序乱了,冲突会明显增多。
6. Cherry-pick 过程中遇到了什么问题
问题 1:shared/contract.ts 冲突
在早期 cherry-pick feat: define document-centered map contracts 时,就遇到了 shared/contract.ts 冲突。
这是很合理的,因为:
main基线里这个文件有自己的状态- 我们要摘过来的提交又在重构 shared contract
处理原则不是“随便选一边”,而是:
- 保留 document-first 架构必须要的 contract
- 同时不要误删
main上仍然需要的部分
冲突解决后,再继续:
git add shared/contract.ts shared/map-document.ts test/frontendToBackendTest/map-document.contract.test.ts
git cherry-pick --continue问题 2:Prisma schema 残留冲突标记
后面虽然主链路能跑,但工作区里还残留了 schema.prisma 的冲突标记。
这个问题危险在于:
- 它未必立刻阻塞 TS build
- 但会污染数据库 schema 真相
- 合并前如果不清掉,就是明显事故遗留
所以我们专门做了一次清理,把 backend/prisma/schema.prisma 整理成干净的 document-first 版本,并单独作为修复处理。
问题 3:基于 main 的 UI 回归
即便数据结构已经搬运成功,UI 仍然出现了回归:
- Profile 入口丢失
- Graph 交互不完整
- 连线不明显
- 侧边栏和主页面布局与记忆中的
main不一致
这里说明一个非常重要的事实:
cherry-pick 解决的是“正确搬运提交”的问题,它不自动保证最终用户体验等价。
所以我们后面又补了多次 fix 提交,专门修基于 main 的 UI 可用性。
7. 这次 Cherry-pick 的本质是什么
这次 cherry-pick 的本质,可以概括成一句话:
我们不是在复制一个分支,而是在重建一条正确历史。
也就是说:
- 不是把旧分支原样搬过来
- 而是把“真正有价值的提交”重新安放到正确基线之上
这就是为什么最后这条分支虽然经历了事故,但仍然能保持比较清晰的原子提交历史。
8. 这次事故带来的教训
教训 1:任何大重构开始前,先确认基线分支
至少要先确认三件事:
git branch --show-current
git log --oneline --decorate -20
git diff main...HEAD --stat确认点包括:
- 当前分支到底从哪里来的
- 当前分支和
main差了什么 - 你记忆中的 UI/功能是否真的存在于这个基线上
教训 2:大改动不要等到最后才验证 UI 基线
这次就是后面才意识到:
- “怎么现在 UI 和 main 不一样?”
如果在更早阶段就和 main 对照一次,事故会小很多。
教训 3:原子提交很重要
如果上午的改动全都混成一坨,后面几乎不可能精准 cherry-pick。
正因为我们有相对明确的提交切分:
- contract
- schema
- backend services
- frontend consumption
- validation
- recovery fixes
所以补救才有操作空间。
教训 4:正确的数据结构改造,仍然可能被错误基线拖累
这次最值得庆幸的是:
- 数据结构方向是对的
MapDocument/ projection / progress 这条路线没有错
出问题的是“承载这次重构的分支基线”,而不是重构目标本身。
9. 这次补救后留下了什么正面结果
虽然过程很折腾,但最终结果其实是好的:
MapDocument成为真相源- MySQL 直接存地图文档 JSON
- projection 成为派生结构
- progress 独立为文档型用户状态
- 分支最终重新建立在
main基线上 - 原子提交历史被保住了,后面可以
rebase and merge到main
这意味着我们不是“勉强把事故盖过去了”,而是把正确架构真正救回到了正确基线上。
10. 如果以后再遇到类似情况,推荐处理流程
推荐流程如下:
- 先停止继续在错误基线上叠加改动
- 回到正确基线,比如
main - 新建干净补救分支
- 先梳理哪些提交是真正想保留的
- 按依赖顺序执行
cherry-pick - 每次冲突都按“最终目标架构”解决,而不是机械选 theirs/ours
- 每搬完一批就 build/test
- 最后再修 UI 回归和残留问题
11. 一句话总结
这次事故的本质不是“代码写错了”,而是:
在错误的基线分支上,做了正确的重构。
而 cherry-pick 的价值在于:
它让我们可以把这些正确的重构,从错误的历史里精确地救出来。