← 返回博客首页

Git 撤销操作完全指南:amend、reset、revert 与 reflog 找回

「提交信息打错了」「刚才那个提交不该提」「本地改的东西想全部丢掉」「reset 完发现删多了」——Git 的撤销需求每一个都很具体,而命令的选择取决于三个问题:改动在哪个层(工作区/暂存/提交)、提交是否已推送、有没有协作者。

这篇把撤销操作按场景矩阵整理,全部命令在临时仓库里真实跑过(输出见「可复现的实测结果」),并覆盖一个常被忽略的边界:untracked 文件与 reset --hard 的关系。


场景矩阵:先对号入座

场景 命令 风险
刚提交,信息有 typo git commit --amend 未推送 = 零风险
撤销未推送的提交,保留改动 git reset --soft/--mixed HEAD~1 零风险(可用 reflog 找回)
撤销未推送的提交,彻底丢弃 git reset --hard HEAD~1 已跟踪文件的未提交改动一并丢失
撤销已推送的提交 git revert <hash> 零风险(新增反向提交)
找回 reset 掉的提交 git reflog + git branch rescue <hash> reflog 过期(默认 90 天)后失效
未跟踪的新文件误删 Git 无法恢复 文件从未进入 Git 对象库

最后一条先划重点:Git 只能恢复进入过对象库的内容。untracked 文件(新建还没 git add 过)被删除时,Git 爱莫能助——本文实测也验证了这一点。


amend:改最近一次提交(含信息与内容)

git commit --amend 把暂存区当前内容与新的提交信息合成一个新提交,替换掉最近一次提交:

echo v1 > a.txt && git add . && git commit -qm "initail commmit"   # typo
git commit --amend -qm "initial commit"
git log --oneline        # → ad04563 initial commit(哈希已变)

实测确认两点:typo 修正生效;哈希完全改变(ad04563 → 新哈希)——amend 本质是「新提交替换旧提交」,不是「修改」。因此它只适用于未推送的提交;已推送的 amend 等同于改写历史。

另外 amend 会把当前暂存区一并并入:如果暂存区有不想提交的改动,先 git reset 取出,再 amend。


reset 三档:改动到底去了哪

git reset 把分支指针移回目标提交,三种模式的区别是工作区与暂存区的改动各自去哪。实测(先提交 a.txt,再依次提交 b.txt、c.txt,然后从 add-c 开始逐档回退):

模式 HEAD 暂存区 工作区 实测 git status
--soft HEAD~1 回退 1 保留(c.txt 在暂存区) 保留 A c.txt
--mixed HEAD~1(默认) 回退 1 清空 保留(取消暂存) ?? b.txt ?? c.txt
--hard HEAD~1 回退 1 清空 丢弃(已跟踪文件) 干净

选法一句话:soft 重新组织提交,mixed 重新编辑,hard 彻底放弃。

一个实测出的真实边界:reset --hard 不会删除 untracked 文件。上面实测中 b.txt 在 mixed 阶段变成未跟踪后,hard 重置并未把它删掉——未跟踪文件不在 Git 对象库与索引的管理范围内。想连它们一起清,用 git clean -fd(先 git clean -nd 预览)。

这也带来一个反直觉的安全提示:hard reset 并不保证「工作区变干净」,untracked 与 ignored 文件会留下来;反过来,它们也因此躲过了 hard 的误删。


revert:唯一安全的「撤销已推送」

git revert <hash> 不移动任何指针,而是计算目标提交的反向补丁并生成一个新提交:

git revert --no-edit HEAD
# → [main 47d7e82] Revert "add-b"
git log --oneline | head -2
#   47d7e82 Revert "add-b"
#   6c3764a add-b

实测确认:add-b 引入的 b.txt 在 revert 后被移除,且历史里两个提交都在——没有改写任何东西,任何人 pull 之后状态一致。代价是历史里会多一条「撤销记录」,但这正是可审计性的来源。

选择铁律由此而来:提交已推送(尤其是共享分支)→ revert;仅存在于本地 → reset。对共享分支 reset 后强推,是把「我的历史」强加给所有协作者,属于最常见的团队事故之一(详见 Git Rebase vs Merge 的黄金法则一节)。


reflog:找回你以为丢掉的提交

git reset --hard 删掉的提交并没有立刻消失——git reflog 记录了 HEAD 的每一次移动(默认保留 90 天)。实测找回流程:

git reset --hard HEAD~1          # 误操作:回退掉 add-b
git log --oneline | head -1      # → 6c3764a add-b(当前 HEAD)
git reflog | head -4
#   6c3764a HEAD@{0}: reset: moving to HEAD~1
#   47d7e82 HEAD@{1}: revert: Revert "add-b"   ← 要找回的提交还在
git branch rescue 47d7e82        # 一条命令找回
git log rescue --oneline | head -1
# → 47d7e82 Revert "add-b" ✓

实测确认被 reset 掉的提交仍可通过 reflog 定位并建成分支。三个注意点:reflog 是本地的(不随 push 传输,别人的仓库没有你的记录);过期条目会被 gc 回收;未提交的改动不在其中——它们从未进入对象库,这也是「先提交再折腾」的真正含义。


可复现的实测结果

以下全部输出来自一次性创建的临时仓库(命令可直接照跑):

① amend 修正提交信息:

echo v1 > a.txt && git add . && git commit -qm "initail commmit"
git commit --amend -qm "initial commit"
git log --oneline
# → ad04563 initial commit   (typo 修正,哈希已变)

② reset 三档实测:

# 从 add-c 开始逐档回退
git reset --soft HEAD~1
#   log: 6c3764a add-b        status: A  c.txt        ← 改动在暂存区
git reset --mixed HEAD~1
#   log: ad04563 init commit  status: ?? b.txt ?? c.txt ← 改动回工作区
git reset --hard HEAD~1
#   log: ad04563 init commit  status: 干净(已跟踪文件)
#   ⚠️ b.txt(untracked)仍然存在 —— hard 不清未跟踪文件

③ revert 生成反向提交:

git revert --no-edit HEAD
# → [main 47d7e82] Revert "add-b"
git log --oneline | head -2
#   47d7e82 Revert "add-b"   ← 反向提交在历史顶部
#   6c3764a add-b            ← 原提交仍完整保留

④ reflog 找回:

git reset --hard HEAD~1          # 误删 revert 提交
git reflog | head -2
#   6c3764a HEAD@{0}: reset: moving to HEAD~1
#   47d7e82 HEAD@{1}: revert: Revert "add-b"
git branch rescue 47d7e82        # 找回 ✓

小结

原则 一句话
先提交再折腾 未提交的改动没有任何恢复手段;提交进对象库后,reflog 是安全网
私有 reset,共享 revert 以「是否已推送/是否有协作者」划线,不看命令顺不顺手
amend 只给未推送 哈希会变,push 过的提交用 revert
untracked 是盲区 reset --hard 不清也不救;新建文件先 git add 进索引
force push 用 lease --force-with-lease 在远端有新提交时拒绝推送

配套命令速查见 Git 命令参考与 git-memo 工具;合并策略的完整对比见 Git Rebase vs Merge。


广告

常见问题

git reset --hard 之后,代码还能找回来吗?

**已提交的部分可以**——`git reflog` 记录了 HEAD 的每一次移动,找到误删前的提交哈希后 `git branch rescue <哈希>` 即可找回(reflog 默认保留 90 天)。**未提交的部分不行**:reset --hard 会清空已跟踪文件的工作区改动,且没有任何恢复手段(untracked 新文件反而会保留,见正文实测)。这也是「先提交再折腾」能救命的原因。

已推送的提交,能用 reset 撤销吗?

**技术上能,团队上不要**。reset 会重写历史,之后必须 force push,其他协作者的本地分支会全部错乱,CI 记录、issue 关联也随之失效。已推送的提交用 **git revert**:它生成一个「反向提交」,历史完整保留、任何人 pull 后状态一致。铁律一句话:**私有分支用 reset,共享分支用 revert**。

git reset 的三种模式到底差在哪?

差别在**改动被撤到哪一层**:`--soft` 只移动 HEAD,改动**保留在暂存区**(重新组织提交用);`--mixed`(默认)移动 HEAD 并清空暂存区,改动**保留在工作区**(重新编辑用);`--hard` 移动 HEAD 且**丢弃所有已跟踪文件的改动**(彻底放弃用,慎用)。实测对比如正文表格。

git commit --amend 会产生新提交吗?

会产生一个**全新的提交**替换原提交——内容可能一样,但哈希完全不同(提交时间戳变了)。对未推送的提交这是安全的「编辑」;对已推送的提交,amend 之后再 push 会被拒绝,需要 force push 并连带上述所有协作者问题。另外注意:amend 会把暂存区当前内容并入提交,amend 前先确认暂存区是干净的。

← 返回博客首页