Git 中的“撤销”不是一个动作:有时只是取消暂存,有时要丢弃文件修改,有时需要撤回本地提交,还有时错误已经推送到多人使用的分支。命令选错,轻则改动仍在,重则覆盖尚未备份的工作。

安全选择命令只需先回答两个问题:

  1. 改动现在位于工作区、暂存区、本地提交,还是远程历史?
  2. 这段历史是否已经推送并被他人共享?

操作前先运行 git status。涉及 reset --hard 或强制推送时,再用 git log --oneline --graph --decorate -n 10 确认目标提交。未提交的修改通常没有可靠备份。

1. 看懂 Git 的四个位置

文件从编辑到协作,通常依次经过以下位置:

先用三个命令定位改动:

1
2
3
git status
git diff
git diff --staged
  • git diff 显示工作区相对暂存区的变化;
  • git diff --staged 显示暂存区相对 HEAD 的变化;
  • git log --oneline 显示已经提交的历史。

2. 先用决策图选择命令

最重要的原则是:未共享的历史可以整理,已经共享的历史优先追加修正,而不是改写过去。

3. 还没有提交:使用 git restore

3.1 丢弃未暂存的文件修改

如果文件只在工作区中修改,想恢复到暂存区记录的状态:

1
git restore <file>

例如:

1
git restore src/config.js

这会覆盖该文件当前的工作区内容。确认修改不再需要后再执行,因为普通 Git 历史通常无法找回从未提交、也未暂存为对象的内容。

如果只想恢复文件中的部分改动,可以交互式选择:

1
git restore -p <file>

3.2 取消暂存,但保留文件修改

文件已经执行过 git add,但暂时不想把它放入下一次提交:

1
git restore --staged <file>

效果是把文件从暂存区取出,工作区修改仍然存在。可以继续编辑、拆分提交或稍后重新暂存。

1
2
执行前:HEAD ── 文件已暂存,工作区也有修改
执行后:HEAD ── 暂存被取消,修改仍留在工作区

如果既要取消暂存,又要丢弃文件修改,应分两步执行,让每一步都清楚可见:

1
2
git restore --staged <file>
git restore <file>

3.3 从指定提交恢复文件

只想把某个文件恢复到旧版本,而不移动整个分支:

1
git restore --source=<commit> -- <file>

例如恢复到上一次提交中的版本:

1
git restore --source=HEAD~1 -- README.md

此命令只是把旧内容写入工作区,之后仍需检查并创建新提交。-- 用来明确分隔“提交名”和“文件路径”,可避免歧义。

4. 已经提交但尚未共享:使用 commit --amendreset

4.1 修正最近一次提交

如果只是漏了文件,或提交说明写错,并且该提交还没有被他人使用:

1
2
3
4
5
# 暂存遗漏的修改
git add <file>

# 修改最近一次提交;编辑器会打开提交说明
git commit --amend

只修改提交说明可使用:

1
git commit --amend -m "fix: correct validation logic"

--amend 会生成一个新的提交来替换原提交,因此已经推送到共享分支后不要随意使用。

4.2 理解 reset 的三个模式

假设当前历史为 A → B → CHEAD 指向错误提交 C。执行 git reset <mode> HEAD~1 后,分支指针回到 B,差别在于是否保留 C 的文件内容:

命令分支指针暂存区工作区典型用途
git reset --soft HEAD~1回到前一提交保留改动保留改动重新组织或合并提交
git reset --mixed HEAD~1回到前一提交取消暂存保留改动重新选择要提交的文件
git reset --hard HEAD~1回到前一提交丢弃改动丢弃改动完全放弃提交及其文件变化

--mixed 是默认模式,因此下面两条命令等价:

1
2
git reset HEAD~1
git reset --mixed HEAD~1

HEAD~1 表示当前提交的第一父提交,也就是通常所说的“上一个提交”。如果目标不是最近一次提交,应先运行 git log --oneline,再使用明确的提交哈希。

git reset --hard 会直接覆盖已跟踪文件的工作区和暂存区内容,但通常不会清理普通的未跟踪文件;清理未跟踪文件是 git clean 的职责。若只是想整理提交,优先从 --soft 或默认的 --mixed 开始。

5. 已经推送到共享分支:使用 git revert

reset 通过移动分支指针来改写历史;revert 则保留原提交,并新增一个内容相反的提交。对于 main 等多人使用的分支,后者通常更安全。

1
2
原历史: A ── B ── C(C 引入错误)
revert: A ── B ── C ── C'(C' 抵消 C,但历史仍完整)

撤销最近一次提交:

1
2
git revert HEAD
git push origin main

撤销指定提交:

1
2
3
git log --oneline
git revert <commit-hash>
git push

如果后续提交修改了同一位置,revert 可能产生冲突。处理方法与普通冲突类似:

1
2
3
4
git status
# 编辑并解决冲突
git add <resolved-file>
git revert --continue

想取消正在进行的撤销操作:

1
git revert --abort

撤销较早提交时,Git 只尝试反转该提交引入的差异,不会自动删除它之后的所有提交。完成后必须运行测试,确认当前组合仍然正确。

6. 个人远程分支:确需改写时使用安全强推

如果提交只位于自己的功能分支、没有其他人基于它开发,可以先在本地 resetrebase,再更新远程分支:

1
2
3
git reset --soft HEAD~1
# 重新整理并提交
git push --force-with-lease

不要直接使用 git push --force--force-with-lease 会检查远程分支是否仍是你预期的状态;如果他人刚推送了新提交,它会拒绝覆盖。

即便如此,也不要对 main、发布分支或多人共同开发的分支强制推送。共享分支出现错误时,优先使用 revert

7. 后悔执行了 reset:使用 reflog 找回提交

提交被 reset 移出当前分支后,通常不会立刻从仓库消失。reflog 记录了本地 HEAD 和分支引用近期移动的位置:

1
git reflog

找到误操作前的提交哈希后,先创建救援分支是更稳妥的做法:

1
2
git branch rescue/<name> <commit-hash>
git switch rescue/<name>

确认内容无误后,再决定合并、挑选提交或移动原分支。不要一上来再次执行 reset --hard,否则会让排查更复杂。

reflog 主要是本地记录,而且有过期与清理机制;它不是备份方案。未提交的工作区修改也不保证能通过 reflog 找回。

8. 常见场景速查

目标推荐命令文件修改是否保留
丢弃某文件未暂存的修改git restore <file>
取消某文件暂存git restore --staged <file>是,留在工作区
从旧提交取回某文件git restore --source=<commit> -- <file>旧内容写入工作区
修正尚未共享的最近提交git commit --amend是,进入替换后的提交
撤销本地提交并保留暂存git reset --soft HEAD~1是,留在暂存区
撤销本地提交并取消暂存git reset HEAD~1是,留在工作区
完全丢弃本地提交与修改git reset --hard HEAD~1
撤销已经共享的提交git revert <commit>以新提交反向修改
找回被移动出分支的提交git reflog取决于目标提交内容

最后记住一句话:restore 处理文件和暂存状态,reset 移动本地历史,revert 安全撤销共享历史,reflog 为本地误操作提供救援线索。

参考资料