Git 误操作拯救指南:改错了、提交错了、推送错了怎么办

写在前面

每个用 Git 的人都有那样的时刻:Enter 落下去半秒之后,大脑才反应过来——reset --hard 的对象好像选错了;或者 push 完盯着屏幕,发现 .env 正安静地躺在提交列表里;又或者一段改了两天的工作,在某个“清理命令”之后消失了。误操作不是“会不会发生”的问题,是“什么时候发生”的问题。

这是 Git 三部曲的第三篇:《Git 命令速查手册》负责“查命令”,《深入理解 .git 目录》负责“懂原理”,这一篇负责“救事故”——把最常见的误提交、误删除、误重置,按 事故深度 整理成可以对照执行的补救手册。全文 18 个案例,每个案例都是“事故现场 → 补救步骤 → 每步验证”的完整流程,可以直接照着敲。

Git 几乎所有操作都可逆——但“怎么逆”取决于坏改动走到了哪一层。 区域判断错了,救命的命令就会变成销毁证据的命令:reset --hard 救过无数人,也毁过无数人的下午。

理解了这一点,急救就有了统一方法论:先定位区域,再选命令,每一步都要验证。 下文的章节顺序,就是事故由浅到深的顺序。


一、先定位,再动手:事故深度决定补救姿势

急救的第一步不是敲命令,而是回答一个问题:我的错误改动(或错误操作)渗透到了哪一层? Git 的四个区域,在事故视角下就是四个深度档位:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
事故现场,从上往下问:
① commit 过吗?──否──▶ 改动还在工作区 / 暂存区          ──▶ 第二节(最浅,也最脆弱)
      │ 是
② push 过吗?───否──▶ 只在本地仓库                      ──▶ 第三节(黄金补救区)
      │ 是
③ 在最新提交,还是更早?──最新──▶ 已推送,但只动最新      ──▶ 第四节(改写 + 力推)
      │ 更早
④ 只是删掉就行,还是必须从历史抹除?                     ──▶ 第五节(rebase)/ 第六节(filter-repo)
  任何一步“提交不见了 / 分支没了” ──▶ 第七节 reflog(最后的底牌)

三个贯穿全文的原则,先立在这里:

  1. 补救命令大多本身就是危险命令。 reset --hardrebasepush --force 都能救命,也都能毁仓库——区别只在于你是否先做了定位。
  2. 每一步补救都配一条验证命令。 本文所有案例都遵循“执行 → 验证 → 下一步”的节奏,照着做就不会在中途迷路。
  3. 深度越深,涉及的人越多。 工作区的事故只关乎你自己;推送之后的事故关乎所有拉过这条分支的人;密钥泄露则关乎整个系统。

二、最浅的事故:改动还在工作区与暂存区

这一层的共同点:大部分改动还没有提交快照。好处是补救零成本;坏处是一旦丢弃,能救回多少取决于它进没进过暂存区——从未 add 过的确实无解,add 过的还有一线生机(见 2.4)。

2.1 案例 1:改错了文件,还没有 add

事故现场:改了几个文件,发现方向完全错了,想丢弃这些改动、回到上次提交的样子。

1
2
3
4
git status                       # ① 先看清要丢弃的范围
git restore src/Program.cs       # ② 丢弃指定文件的未暂存改动
git restore .                    #    (丢弃当前目录下所有未暂存改动)
git status                       # ③ 验证:工作区应恢复干净

只想丢弃部分改动、保留部分?用 git restore -p(patch 模式)按块挑选,和 git add -p 是同一套交互。

2.2 案例 2:add 了不想提交的东西

事故现场:手滑 git add .,把调试文件、临时脚本一起放进了暂存区——但改动本身还要,只是不该现在提交。

1
2
3
git restore --staged debug.log   # ① 退出暂存区,改动保留在工作区
git diff --cached --name-only    # ② 验证:暂存区里已无此文件
git status                       #    该文件应显示为未暂存的修改(M 在右列)

注意 restorerestore --staged 的分工:前者动工作区,后者动暂存区。后者只是“退出”,什么都不丢

2.3 案例 3:改动舍不得丢,但马上要切分支

事故现场:功能写了一半,突然要切到别的分支修个紧急 bug——半成品不能提交,也不能丢。

1
2
3
4
5
6
7
git stash push -u -m "登录功能做到一半"   # ① 连未跟踪文件一起收起(-u)
git status                                # ② 验证:工作区干净,可以安全切分支
git switch hotfix/urgent                  # ③ 去处理别的事
# ……修完回来:
git switch feature/login
git stash list                            # ④ 验证:暂存记录还在
git stash pop                             # ⑤ 恢复并删除该条暂存记录

pop 只在恢复成功、没有冲突时才删除该条记录;如果恢复时发生冲突,记录会保留,需要解决冲突后再用 git stash drop 手动清理。apply 则总是保留记录(想多处应用或多一层保险时用)。收起时 务必带 -m 说明 ——三条以上的匿名暂存摆在一起,你不会记得哪条是哪条。

2.4 警告:这一层丢掉的东西,能救回多少取决于进没进过暂存区

1
2
3
4
5
# ❌ 用硬重置去“清理”未提交的改动
git reset --hard                # 把工作区和暂存区的改动一并丢弃

# ✅ 想丢弃,先确认真的不要了;不确定就先 stash 或 commit 一版
git stash push -u -m "先存一版再说"

要分两种情况:

  • 从未 add 过的改动:只存在于工作区文件里,Git 没有为它创建任何对象——reflog 里不会有它,垃圾回收也无从谈起,真的无解
  • add 过又被丢弃的改动git add 的那一刻,内容已经写成了对象库里的 blob;即使后来从暂存区丢弃,在被垃圾回收清掉之前,还可能用 git fsck --lost-found 找回这些悬空(dangling)对象——输出会写进 .git/lost-found/。代价是恢复难度高、原文件名会丢失,只能当最后的碰运气手段。
1
git fsck --lost-found           # 列出悬空对象,找回的 blob 写入 .git/lost-found/

所以规矩是:不确定要不要丢,就先 stash 或 commit 一版——工作区层的事故,预防远比补救可靠(见第八节)。


三、黄金补救区:已提交、未推送

如果错误已经 commit 但还没 push——恭喜,这是 最好的事故状态:历史完全属于你,随便改,没人知道。这一节的六个案例覆盖了日常 80% 的误操作。

3.1 案例 4:提交信息写错了

事故现场:commit 完一看,信息里有错别字,或者写了个毫无信息量的 “fix bug”。

1
2
git commit --amend -m "fix: 修复购物车金额为负时无法结算的问题"   # ① 重写信息
git log -1                                                       # ② 验证:信息已更新

3.2 案例 5:提交完发现漏了一个文件

事故现场:刚提交完,发现还有一个文件忘了 add,不想为它再开一个碎提交。

1
2
3
git add src/Forgot.cs            # ① 把漏掉的补进暂存区
git commit --amend --no-edit     # ② 并入上一次提交,信息不变
git show --stat HEAD             # ③ 验证:该文件已出现在这条提交里

3.3 案例 6:提交里混进了不该提交的文件(还没推送)

事故现场:git add . 一把梭,提交完才发现本地配置 config/local.settings.json.env 之类的文件混了进去。

1
2
3
4
5
6
git rm --cached config/local.settings.json   # ① 只从索引移除,磁盘文件保留
echo "config/local.settings.json" >> .gitignore
git add .gitignore                          # ② 顺手补上忽略规则,防止下次再犯
git commit --amend --no-edit                 # ③ 并入上一次提交
git show --stat HEAD                         # ④ 验证:该文件已不在提交里
git status --short --ignored                 #    该文件显示 !!(已被忽略,仍保留在本地)

rm --cached 是这一步的灵魂:只动索引,不动磁盘。如果手滑执行了不带 --cachedgit rm,文件会被同时从工作区删除——那就先 git restore --staged --worktree <file> 找回,再重来。另外注意:因为同一步把它写进了 .gitignore,普通的 git status --short 不会再显示它——被忽略的未跟踪文件要加 --ignored 才看得见。

3.4 案例 7:整个提交都不要了,但改动想留着

事故现场:刚提交完就后悔——这些改动还没到能提交的状态,想收回继续改。

1
2
3
git reset --soft HEAD~1         # ① 撤销提交,改动原样保留在暂存区
git status                      # ② 验证:改动都在(绿色,已暂存)
git log --oneline -3            #    那条提交已消失

3.5 案例 8:整个提交连同改动一起不要

事故现场:提交的内容完全错误,改动本身也是垃圾,想彻底回到上一条提交。

1
2
3
git log --oneline -3            # ① 先看清要退到哪
git reset --hard HEAD~1         # ② 撤销提交 + 丢弃全部改动
git status                      # ③ 验证:工作区干净,HEAD 指向上一条提交

⚠️ 这是本文第一个高破坏性命令:已提交的内容通常还能靠 reflog 兜底(见 7.1),但它会 无警告丢弃工作区未提交的改动 ——那部分一旦没 add 过就真没了(2.4)。执行前务必完成 ① 的确认:reset --hard 最常见的事故就是没看清回退目标。

3.6 案例 9:晚了三个提交才发现,想把它们合并成一个

事故现场:紧张地连点了三个"WIP"“fix"“真正修复”碎提交,推送前想整理成一个完整提交。

1
2
3
4
5
git log --oneline -4            # ① 确认要合并的范围(最近 3 条)
git reset --soft HEAD~3         # ② 三条提交一起退回暂存区
git status                      # ③ 验证:三个提交的全部改动都堆在暂存区
git commit -m "feat: 完成登录令牌刷新逻辑"
git log --oneline -2            # ④ 验证:三条变一条

reset 三种模式差别一张表看全(更详细的解释见速查手册 4.1):

模式工作区暂存区一句话
--soft保留保留只退提交,改动原地待命
--mixed(默认)保留清空退提交 + 退 add,重新挑选
--hard丢弃丢弃一切归零(危险)

四、已推送:历史不再只属于你

push 之后,游戏规则变了:远端历史是所有协作者的公共财产,改写它需要付出协调成本。这一节先给一个完整的真实案例(改编自一次实际事故,细节已脱敏),再给两个变体,最后讲清楚力推的安全阀。

4.1 案例 10(主案例):架构文档误进了 feature 分支,已推送

事故现场:在 feature/grpc-gateway 上开发时,把架构评审稿 docs/architecture-notes/ 顺手 git add . 进了 最新提交,并且已经 push 到远端。目标:从这条提交里摘掉文档目录——本地文件保留、其它未提交的代码改动保留、提交信息不变——然后覆盖远端。

完整手术,每一步都可以照敲:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
git branch --show-current       # ① 确认在 feature/grpc-gateway 上动手(改历史前必查)

git restore --staged :/         # ② 清空手术台:暂存区整体对齐 HEAD
git diff --cached --name-status # ③ 验证一:应无输出(暂存区为空,没有杂物会被卷进后续提交)

git rm -r --cached docs/architecture-notes   # ④ 只从索引摘掉文档目录,磁盘保留
git diff --cached --name-status              # ⑤ 验证二:应只有 D 开头的该目录记录

git commit --amend --no-edit     # ⑥ 按当前暂存区重做最新提交(= 旧提交 − 文档目录)
git show --stat HEAD             # ⑦ 验证三:新提交的文件列表里已无该目录

git push --force-with-lease origin feature/grpc-gateway   # ⑧ 覆盖远端(带保险,见 4.4)
git status --short               # ⑨ 终态:M = 原有代码改动还在;?? = 文档目录转未跟踪

这套手术的原理:commit --amend 定型的是暂存区,不是工作区。 第 ② 步先把暂存区对齐 HEAD(清掉无关内容),第 ④ 步再从索引摘掉目标目录,于是 amend 出的新提交恰好等于“旧提交 − 文档目录”,多一分少一分都没有。三道验证闸门分别防的是:杂物混入(③)、误伤其它文件(⑤)、摘除不彻底(⑦)。

4.2 案例 11:推送后发现提交信息里有敏感内容

事故现场:提交信息里带了内网地址或不该公开的细节,已经推送。

1
2
3
git commit --amend -m "chore: 更新网关配置"    # ① 重写信息
git log -1                                    # ② 验证
git push --force-with-lease                   # ③ 覆盖远端

4.3 案例 12:代码推错了分支(推到了共享的 main)

事故现场:本想推到 feature/grpc-gateway,结果推到了 main——而 main 是共享分支,不能改写历史

1
2
3
git log origin/main --oneline -3          # ① 确认误推了哪些提交
git revert <误推的提交SHA>                 # ② 生成一条“反向提交”,抵消错误内容
git push origin main                      # ③ 正常推送(没有改写任何历史)

共享分支的铁律:revert 新增反提交,而不是 reset + 力推。 反提交不动任何人的历史,所有人拉取即可对齐;力推则会让每个已经拉取过 main 的人本地分叉。只有当分支确实只有你一个人用(比如上一节的个人 feature 分支),才轮到“改写 + 力推”方案。

误推的是连续多条提交时,不必一条条 revert:git revert --no-commit <最早SHA>^..<最新SHA> 把整段一起反提交,再 git commit -m "revert: 撤销误推到 main 的提交" 合成一条。若误推里含 merge 提交,不能按区间直接处理,要用 git revert -m <父编号> <merge的SHA> 指定沿哪条主线撤销。

4.4 --force--force-with-lease:力推的安全阀

改写历史后的推送必然被远端拒绝(非快进),这时需要力推。两个命令的差别值得单独讲:

1
2
3
4
5
6
7
git push --force:
  "我说覆盖就覆盖" ──▶ 如果同事在你上次 fetch 之后推过新提交,会被无声碾掉

git push --force-with-lease:
  "远端必须仍是我上次见到的样子"(以本地远程跟踪引用为准)
     ├── 一致 ──▶ 允许推送
     └── 不一致(远端有我没见过的更新)──▶ 拒绝,提示你先 fetch 看看

两个诚实的提醒:

  • --force-with-lease 防的是“覆盖 你没见过 的更新”。一旦你 fetch 过,本地的跟踪引用随之更新,lease 检查就会放行——所以正确节奏是“fetch → git log 看一眼远端新提交 → 决定要不要 force”,而不是 fetch 完直接无脑力推。
  • 力推前自问三个问题:这条分支只有我在用吗?有没有开着的 PR?有没有 CI 或别人基于旧提交工作?任何一个是“有”,先协调再动手。

五、更深一层:坏提交在更早的历史里

事故现场升级:有问题的提交不是最新的那条,而是三个提交之前的一条。此时 amend 够不着了,需要 rebase -i(交互式变基)回到过去动手术。

5.1 案例 13:三个提交前混进了配置文件

1
2
git log --oneline -4            # ① 找到目标提交的位置
git rebase -i HEAD~3            # ② 打开变基清单

编辑器里会看到(从旧到新排列):

1
2
3
pick a1b2c3 feat: 网关路由骨架        ← 目标:这条里混进了 config/local.settings.json
pick d4e5f6 fix: 修复路由匹配
pick 7g8h9i test: 补充路由测试

把目标行的 pick 改成 edit,保存退出。Git 会 停在 那条提交上:

1
2
3
4
5
# (rebase 已停在 a1b2c3)
git rm --cached config/local.settings.json   # ③ 从这条提交里摘掉文件
git commit --amend --no-edit                 # ④ 定型修改
git rebase --continue                        # ⑤ 重放后续提交,直到完成
git log --all --oneline -- config/local.settings.json   # ⑥ 验证:全部引用中该路径已无记录(应无输出)

验证要用 --all 而不是只看当前分支——如果这份配置也曾进过别的分支,单看 HEAD 会漏判。

5.2 案例 14:中间某个提交整个不要了

1
2
3
4
git rebase -i HEAD~3
# 把不要的那条 pick 改成 drop(或直接删掉那一行),保存
git rebase --continue           # 重放其余提交
git log --oneline               # 验证:该提交已消失,其余保留

rebase 是“换底重放”:被编辑范围之内的提交都会 改写提交 ID(对象与引用的机制见《深入理解 .git 目录》)。因此黄金法则不变——只 rebase 还没推送的提交;已经推送的,要么协调后统一改写,要么退回 revert 方案。


六、彻底抹除:密钥与大文件

前面所有手段的共同局限:只是让最新视图里看不到,并不等于从仓库里消失。 被 amend / rebase / reset 淘汰的旧提交,要么还被 reflog、其它分支、标签或远端跟踪引用牵着,要么只是变成对象库里暂时无人认领的不可达对象、在被清理前一直躺在磁盘上;而所有克隆过的副本,完整保留着旧历史的每一份内容。密钥被推送过一次,就等于已经离开过你的机器。这一节处理最深的两种事故。

6.1 案例 15:API 密钥提交并推送了

处理顺序本身就是知识点:

1
2
3
4
① 立即作废并轮换密钥        ← 第一优先级!推送过的密钥视为已泄露
   (换新密钥、吊销旧密钥、检查有无异常调用)
② 从所有历史中抹除
③ 通知协作者按流程清理(重新克隆最稳,或严格变基)

为什么轮换优先于清理:密钥进入远端的那一秒就可能被抓取(爬虫、fork、CI 缓存、别人的 fetch)。事后无论 Git 手术做多干净,都改变不了“它曾经公开存在过”这个事实。清理只是止损的收尾,不是止血;轮换及时且充分时,历史清理甚至可以量力而行——GitHub 官方也是这个口径。

抹除使用 git filter-repo(官方推荐的现代工具,取代了慢且易错的 filter-branch)。它不是 Git 核心自带的工具,需要单独安装(安装后通过 git filter-repo 外部子命令的方式调用):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
pip install git-filter-repo            # ① 安装;敏感数据场景需要 2.47+(--sensitive-data-removal 自该版本起提供)

# ② 全新克隆一份干净副本,在副本上动手术
git clone https://github.com/team/repo.git repo-clean
cd repo-clean

# ③ 从全部分支、标签的完整历史中移除该路径;--sensitive-data-removal 附带额外的清理与防护
git filter-repo --sensitive-data-removal --invert-paths --path config/api-keys.json

# ④ 推送前先统计这次改写会影响多少 PR(过了这一步,改写就不可逆了)
grep -c '^refs/pull/.*/head$' .git/filter-repo/changed-refs

# ⑤ filter-repo 重写历史后会刻意移除 origin、防止把重写结果误推回原仓库——推送前先检查,缺了就补回来
git remote -v
git remote add origin https://github.com/team/repo.git    # 若上一步输出里没有 origin,才需要执行

# ⑥ 把改写后的全部引用(分支 / 标签等)整体回推远端
git push --force --mirror origin

四个容易踩的细节:

  • 文件改过路径的,每条历史路径都要列:文件曾被移动或重命名,就要为每个旧路径补一个 --path(或分两次执行),否则旧路径上的密钥仍留在历史里。
  • refs/pull/ 推送失败是预期行为:GitHub 把 PR 引用设为只读;若还有其它引用推送失败,多半是分支保护拦住了力推,需临时关闭后重推。
  • 力推清不掉的部分要找 Support:PR 引用与 GitHub 的缓存视图不随力推消失,需带仓库信息、受影响 PR 数量和 filter-repo 输出的 First Changed Commit 联系 GitHub Support 清理;fork 里的副本要联系 fork 所有者处理。
  • 协作者必须变基(rebase),不能合并(merge):一次 merge 就可能把刚清除的污染历史原样带回来。最省事、最不易复发的做法是丢弃旧克隆、重新克隆。

6.2 案例 16:大文件让仓库膨胀了几百 MB

事故现场:早期误提交了视频、数据集或 node_modules,仓库越来越大,clone 越来越慢。

1
2
3
4
5
6
7
8
# ① 同案例 15,在全新克隆上操作:先找出仓库里最大的对象(filter-repo 自带分析工具)
git filter-repo --analyze
ls .git/filter-repo/analysis/          # 生成一组体积报告,按目录 / 文件名排序定位大文件

# ② 按路径抹除(支持通配)
git filter-repo --invert-paths --path-glob 'assets/videos/*'

# ③ 回推与善后同案例 15:--mirror 力推、Support 清缓存、协作者重新克隆或变基

注意:filter-repo 会改写 全部相关提交的 ID,这是自案例 13 以来“改写历史”的极端形态——协作成本最高,值得为它单独发一次全员通知。另外,托管平台(如 GitHub)可能仍缓存旧对象,重要密钥泄露后可联系平台支持清理缓存视图。


七、最后的底牌:reflog

到目前为止的所有“不可逆”,其实都还有一个兜底:reflog(reference log,引用日志)——Git 为本地引用自动记录的更新日志,HEAD、每个本地分支、stash 各自都有一份(不带参数的 git reflog 显示的就是 HEAD 的那份,下文示例用的都是它)。reset --hardrebasebranch -D 都只是让提交“不再被分支指向”,对象本身还躺在对象库里,reflog 里的记录还能找到它们

想理解 reflog 为什么存在、记录在 .git 的哪里,见《深入理解 .git 目录》;这一节专注“怎么用它救命”。

7.1 案例 17:reset --hard 错杀了想要的提交

事故现场:本想丢弃垃圾改动执行了 git reset --hard HEAD~1,退完才发现——那条提交里有没保存的工作。

1
git reflog                      # ① 查看 HEAD 的移动历史(最新在上)
1
2
3
4
a1b2c3d (HEAD -> main) HEAD@{0}: reset: moving to HEAD~1     ← 事故:重置到这里
e4f5g6h HEAD@{1}: commit: 完成网关鉴权中间件                  ← 错杀的提交!
9i0j1k2 HEAD@{2}: commit: feat: 路由骨架
...
1
2
git reset --hard e4f5g6h        # ② 回到事故前的提交(或 git branch rescue e4f5g6h 更稳妥)
git log --oneline -2            # ③ 验证:提交失而复得

想更稳妥,用 git branch rescue e4f5g6h 把它标记成一个新分支——先保住现场,再慢慢决定怎么处置,比直接再 reset --hard 来回跳安全得多。

7.2 案例 18:误删了还在开发的分支

事故现场:手滑 git branch -D feature/login,删完想起里面有三条没合并的提交。

1
2
3
git reflog | head -20           # ① 在 HEAD 日志里定位该分支的尖端
git branch feature/login <SHA>  # ② 用那个 SHA 重建分支
git log --oneline feature/login -3   # ③ 验证:提交都在

定位 SHA 的方法:git reflog 默认最新记录在最上面、越往下越早。先找到 checkout: moving from feature/login to … 这一行——注意这行本身的 SHA 是离开后的目标分支(如 main),不是 feature/login 的尖端;分支尖端是紧挨它下方的那条更早记录(也就是 HEAD@{n+1}),即切换前 HEAD 所在的提交——那条记录是 commit:merge: 还是别的类型都无所谓。翻不到的话,退一步用 git fsck --lost-found 找悬空提交(思路同 2.4)。

7.3 reflog 的三条边界

  1. 它是本地的。 记录的是这台机器上的引用更新,clone 时不会带过来;而且删除分支时,该分支自己的那份 reflog 会随之删除——所以案例 18 只能借 HEAD 的日志找人。
  2. 它有期限。 默认可达记录保留 90 天、不可达记录 30 天,git gc 过期清理——所以救场要趁早。
  3. 它只认有对象的东西。 从未 add 过的工作区改动连对象都没有,reflog 和 fsck 都无能为力;add 过再丢弃的还有 fsck --lost-found 一线生机(回扣 2.4);stash 清空后的记录一旦过期同样无处可寻。

八、预防:最好的急救是不用急救

  • .gitignore 第一天就配齐。 新仓库落地的第一件事不是写代码,是把 bin/obj/node_modules/.env*.local.* 这些“未来的事故”挡在门外。经验法则:git status 里长期挂着某个 ??,它早晚会混进某次 git add .
  • 提交前预览,是性价比最高的仪式。 git add 之后、commit 之前,花五秒看一眼 git diff --cached --stat——本文一半以上的事故(混入文件、漏掉文件)都能在这一眼里拦住。
  • pre-commit 钩子做自动安检。pre-commit 框架挂 gitleaks 扫密钥、挂大小检查拦大文件,把“人肉记得”变成“机器记得”。
  • 推送想一秒再敲。 推送是发布:本地历史怎么改都是你自己的事,推送之后就进入公共领域。分支名、目标远端、提交内容,过一眼再回车(git push 之前 git log origin/<分支>..HEAD --oneline 看看将要推出去什么)。

九、急救决策表

把全文收拢成一张表。用法:先在“症状”列对号入座,再执行对应补救,危险级高的先回看对应章节。

症状所在区域首选补救危险级章节
改错文件,还没 add工作区git restore <file>2.1
add 了不想提交的暂存区git restore --staged <file>2.2
想暂时收起改动切分支工作区git stash push -u -m2.3
提交信息写错(未推送)本地仓库git commit --amend -m3.1
提交漏了 / 多了文件(未推送)本地仓库add--amend --no-edit3.2 / 3.3
整个提交不要,改动保留本地仓库git reset --soft HEAD~13.4
提交连同改动全部不要本地仓库git reset --hard HEAD~1★★★3.5
多条碎提交合成一条本地仓库git reset --soft HEAD~N 后重新提交★★3.6
误提交文件且已推送(个人分支)远端rm --cached + amend + --force-with-lease★★★4.1
敏感信息在提交信息里且已推送远端amend -m + --force-with-lease★★4.2
误推到共享分支(如 main远端git revert 反提交,不改写历史4.3
坏提交在更早历史(未推送)本地历史git rebase -i(edit / drop)★★★
密钥已提交并推送历史对象先轮换密钥,再 filter-repo★★★6.1
大文件撑爆仓库历史对象git filter-repo --analyze 后按路径抹除★★★6.2
reset --hard 错杀了提交对象库git reflog 找 SHA 回退 / 建分支7.1
误删了分支对象库git reflog + git branch <名> <SHA>7.2
未提交的改动被丢弃无快照从未 add:无解;add 过:git fsck --lost-found 碰运气★★★2.4

十、小结

  • 改动还在工作区与暂存区时,补救零成本,但丢弃后能救回多少取决于有没有进过对象库——restore / restore --staged / stash 各管一段,reset --hard 不要当清扫工具。
  • 已提交未推送是黄金补救区:amend 改一条、reset --soft 退一批,历史随便重写,因为只有你看得到。
  • 已推送就进入了公共领域:个人分支用“改写 + --force-with-lease”,共享分支用 revert 反提交;手术的核心是“清空手术台(restore --staged)→ 精确摘除(rm --cached)→ 三道验证 → amend → 力推”。
  • 更深的事故交给 rebase -i(回到过去动手术)和 filter-repo(彻底抹除),密钥泄露永远先轮换再清理。
  • reflog 是最后的底牌——本地的 HEAD 移动日志,能在记录过期、对象被清理前给错杀的提交第二次机会;不可达记录默认约 30 天过期,事故后要立即处理。从未 add 过的改动,它也无能为力。
1
2
3
4
记住三句话:
  1. 先定位区域,再选命令——事故深度决定补救姿势
  2. 救命的命令往往也是危险的命令,每一步都要有验证
  3. 共享的历史用 revert,个人的分支才敢 force;密钥泄露先轮换

下次手滑时,先停一秒,回到第一节的分诊图:确认改动走到了哪一层,再挑那一层的刀——你会发现 Git 的“可撤销”,比传说中靠谱得多。

参考资料

Licensed under CC BY-NC-SA 4.0