Appearance
Git 历史错乱(重写后平行宇宙)复盘与通用修复方案
一句话结论:只要出现过
git filter-branch/ 大文件清理 /git push --force改写历史,就一定会出现「代码一模一样但 Hash 不一样」的平行宇宙。此时不要合并、不要 Pull、不要让 AI 随便动历史 —— 唯一正确做法:找到最干净的那条基准分支,把新写的代码 Cherry-pick 过去,废弃其他所有污染分支。
0. 🚨 绝对权限红线:AI 不允许做的事(最重要,记第一条)
这是本次故障最大的教训。历史重写类操作的犯错成本是 10 倍普通 Bug。
后续所有 Git 相关操作,严格对号入座:
| 操作类型 | 允许 AI 做吗? | 备注 |
|---|---|---|
查看提交历史 / git log / git reflog / git diff | ✅ 随便看 | 只读操作,不会搞砸 |
Cherry-pick 单条提交到干净分支、单个分支 git reset --hard origin/xxx | ✅ 可以(先告知步骤) | 局部操作,影响范围可控 |
git push --force / git push origin master --force / git push --tags --force | ⚠️ 只允许在单人项目,且必须先列出要覆盖的分支清单与当前 Commit,等你人工确认一句「执行」才推 | 多人项目直接禁止 AI 执行 Force Push |
任何改写历史的操作:git rebase -i、filter-branch / filter-repo / 删大文件 / 批量改提交信息 | ❌ 绝对禁止 AI 直接执行 | 必须 AI 出方案 → 你看懂 → 复制粘贴手动在终端敲 |
| 涉及多个分支联动的操作(同时改 master/dev/多个 release + tags) | ❌ 禁止 AI 一口气做 | 必须 AI 先出步骤表 → 你一条一条确认后手动或单独执行 |
| 分支整体 Merge(把一个还可能带旧历史的特性分支合进干净分支) | ❌ 连你自己也要禁止,不要犯懒 | 用 Cherry-pick 单条挑,不要直接 Merge 整个脏分支 |
1. 本次故障现象(下次一眼识别)
项目:SiRuiExam(单人开发,3 位同事偶尔拉代码)
典型症状 1:对称的「传入/传出」数量。
release-1.1.07:241 ahead, 241 behindrelease-1.1.08:283 ahead, 283 behind
典型症状 2:master 的提交历史重复得密密麻麻。 同一批代码,出现两次,Hash 不同,作者时间内容全一样。
典型症状 3:明明「代码没冲突」,但两个分支一 Merge 就 20+ 冲突。 (这就是平行宇宙的信号:Git 不认识 Hash,以为两边完全无关。)
2. 根本原因:平行宇宙是怎么产生的
一句话根因:2 月份为了删仓库里的 UploadFiles / ZIP 大文件,执行过历史重写(git filter-repo / BFG 之类的工具)。 它让「同一代码」出现了两套完全不同的 Commit Hash:
| 宇宙 | 身份 | 标志性提交 | 状态 |
|---|---|---|---|
| 宇宙 A(纯净) | 所有旧 release 分支、所有 Tags、远程老仓库最初的真实记录 | ed210076(release-1.1.08 正式发布)、0cef1ec5(旧版清理大文件) | ✅ 正确、所有标签对齐、这才是基准 |
| 宇宙 B(毒药) | 某次本地/同事 Pull 时「两边都合一下」意外带进来的 | 51440eaf、7bf22701(新版重写后对应的清理提交) | ❌ 只要进了一条,就会带几百条「重复影子提交」 |
为什么会把「毒药」合进来? AI 在修复时犯了一个致命错误:它以为本地 release-1.1.08 就等于干净的远程,但实际上本地 release-1.1.08 指针悄悄已经指到宇宙 B 上了(51440eaf)。 结果 dev/master 整个建在了毒药底座上,你后来去合并真正干净的 origin/release-1.1.08,两边不认识,爆 20+ 冲突。
🎯 核心识别方法:
51440eaf以及它之前的所有旧 commit(如0cef1ec5的新重写版7bf22701)都是「毒药」。修复目标是:只要你新写的代码,不要这层毒药底座。
3. 通用修复三步法(下次遇到,照着走)
核心思想就两句:找最干净的基准 → 只摘新写的代码 → 其他所有分支一律废弃不动。 越简单越不容易翻车。
Step 1:先找到「最干净、最干净」的那条基准分支
怎么判定谁是最干净的?按下面的优先级从高到低找,找到就停,不要往下选:
- 看标签:最近一次正式发版的 Tag(如
v1.1.08)绑定在哪个分支上,那个就是最干净的。(因为 Tag 一发布基本没人敢动,历史最稳。) - 看旧 release 分支:越老的
release-1.x.y越没被重写污染,基本都在纯净宇宙 A。 - 看远程是否一致:本地分支和
origin/分支名是不是一条直线?有没有 ahead/behind?完全相等的才可能作为基准。 - 看最早的 commit:翻最老的几个 commit,哪条分支还保留着当初最初的提交 Hash,那就是最干净的。
本次结论:origin/release-1.1.08(Commit ed210076)——它在宇宙 A,所有 Tag 都对齐它 → 基准就是它,永远不要再换了。
Step 2:重建 master / dev,只做 Cherry-pick(不要 Merge 脏分支)
项目只有你一个人开发时,这一步 10 分钟搞定,无任何风险。
powershell
# 1. 先在本地基于干净基准重新拉出 master 和 dev
# (相当于把旧的脏 master/dev 直接扔了重建)
git checkout release-1.1.08
git branch -f dev release-1.1.08
git branch -f master release-1.1.08
# 2. 切到 dev,把脏分支里真正有用的「新写的代码提交」一条条挑过来
git checkout dev
# 示例:只挑数据归档功能的 13 条业务提交
git cherry-pick e7802186
git cherry-pick e41dee94
git cherry-pick ...红线 3 条:
- ❌ 绝对不要图省事
git merge feature-脏分支,一合就把毒药历史全带进来。 - ❌ Cherry-pick 中如果遇到「清理大文件」「合并 master」这类没有具体业务代码的提交,直接跳过(
git cherry-pick --skip)。 - ⚠️ 遇到小冲突:手动改完
git cherry-pick --continue,别慌,冲突只发生在 Cherry-pick 单条的范围内,永远不会像 Merge 那样滚成几百条。
Step 3:废弃其他所有分支,不要试图修好每一条
这一步很多人舍不得,其实是最安全的关键动作。单人项目没有协作风险,越敢扔越不容易复发。
要保留的分支(只留这 3 个,其他一律本地 branch -D) | 要删的 / 要废弃的 |
|---|---|
master ✅ | 所有 release-x.y 在本地可以删(远程保留用来考古,别碰就行) |
dev ✅ | 所有脏的 feature-*(如 feature-1.1.09-数据归档 — 本次的污染源) |
| 当前正在开发的唯一特性分支(基于新 dev 拉) | 本地任何显示 ahead/behind 几百条的旧分支引用 |
powershell
# 旧污染分支,确认代码都 Cherry-pick 完了就删
git branch -D feature-1.1.09-数据归档4. 特殊场景处理(这次踩过的坑,下次别踩)
4.1 强推远程(--force)怎么最安全
只有当你本地 dev/master 已经修干净、并自己跑过一遍代码,确认没问题了再走这步。
powershell
# 先看一眼 --force-with-lease 行不行(推荐优先用这个)
git push origin dev --force-with-lease
git push origin master --force-with-lease
# 如果因为 Tags 或远端还有人推过,lease 失败,再退化成 --force
# 但这一步建议人工盯着执行,AI 不要自己决定
git push origin dev --force
git push origin master --force💡 Force Push 后一定记得推 Tags(如果要统一 Tags 的话):
powershellgit push origin --tags --force
4.2 公司电脑 / 其他电脑怎么避免二次污染(强制重置)
所有在这次 Force Push 之前拉过一次 dev/master 的电脑,本地都有「旧的脏缓存」。他们如果直接 git pull,Git 会自动帮你把脏历史重新 Merge 回去。所以必须走 Reset:
最简单粗暴、对同事最容易解释的方法:
「今天清理了一下服务器冗余历史,大家把本地
dev、master两个分支删了重拉一下就行,不会影响你正在写的代码。」
终端等价命令(不解释给同事,自己用就行):
powershell
git checkout release-1.1.08
git branch -D dev
git branch -D master
git fetch --all
git checkout dev
git checkout masterVS 界面操作:
- 先点「提取(Fetch)」→ 不要点拉取/同步
- 切到
dev→ 右键remotes/origin/dev→ 将 dev 重置为 → 保留更改(Hard) master同上
4.3 Tag 冲突(看不见 / Push 时 tag already exists)
Tag 问题只要记住一句:Tag 和 Commit Hash 是死绑定,只要历史重写过,两边的同名牌 Tag 一定绑定的是不同 Hash,Git 默认不会互相覆盖。
| 场景 | 命令 |
|---|---|
| 家里电脑修好后,把正确的 Tag 强推到远程覆盖 | git push origin --tags --force |
| 公司电脑 Fetch 时用远程正确 Tag 覆盖本地错的 | git fetch --tags --force |
5. 未来 3 条铁律(贴在 Git 命令旁提醒自己)
- 【一超多强】基准只留一个: 不管出什么事,
release-1.1.08(或最新的发版分支)永远是唯一的真理源,其他分支坏了就重拉,不要试图修好每条坏分支。 - 【挑拣代替合并】 新功能从旧分支搬过来,一律 Cherry-pick 单条挑,不要 Merge 整个旧特性分支。这是杜绝「毒药历史」复发的最大保护伞。
- 【AI 只写方案,不动历史】 凡是 Force Push、Rebase、删历史、多个分支联动的操作,AI 只允许出 Markdown 步骤表,你一条一条看了再决定是否手动或单步执行。AI 不能把一串操作一次性跑完。
附:本次 2026-09-07 执行回顾(仅记录,以后复用的是上面 3 步法)
- 识别基准:
origin/release-1.1.08(ed210076)→ 最干净,对齐所有 Tag - Cherry-pick 数量:13 条业务提交(
e7802186 → aee03ff8),跳过所有合并提交和清理大文件的提交 - Force Push 结果:
dev和master回到宇宙 A,git merge release-1.1.08显示Already up to date. - 遗留处理:旧
feature-1.1.09-数据归档分支已标记废弃,确认新代码无异常后本地+远程均删除
