文章目录
- [Git 版本回退与撤销:写错代码后如何优雅反悔](#Git 版本回退与撤销:写错代码后如何优雅反悔)
-
- 一、为什么需要版本回退
- [二、git reset 的三种模式](#二、git reset 的三种模式)
-
- [1. --soft:只移动版本库指针](#1. --soft:只移动版本库指针)
- [2. --mixed:回退版本库和暂存区](#2. --mixed:回退版本库和暂存区)
- [3. --hard:版本库、暂存区、工作区全部回退](#3. --hard:版本库、暂存区、工作区全部回退)
- [三、HEAD、commit id 和版本定位](#三、HEAD、commit id 和版本定位)
-
- [1. 使用 commit id](#1. 使用 commit id)
- [2. 使用 HEAD](#2. 使用 HEAD)
- 四、实战:回退到旧版本,再找回新版本
-
- [1. 回退到 version2](#1. 回退到 version2)
- [2. 回退之后又想回到 version3 怎么办](#2. 回退之后又想回到 version3 怎么办)
- [3. git log 和 git reflog 的区别](#3. git log 和 git reflog 的区别)
- 五、撤销修改:按文件所在区域处理
-
- [1. 只修改了工作区,还没有 add](#1. 只修改了工作区,还没有 add)
- [2. 已经 add,但还没有 commit](#2. 已经 add,但还没有 commit)
- [3. 已经 commit](#3. 已经 commit)
- 六、删除文件:恢复误删与彻底删除
-
- [1. 文件误删:恢复回来](#1. 文件误删:恢复回来)
- [2. 文件确实不要了:从版本库删除](#2. 文件确实不要了:从版本库删除)
- [3. rm 和 git rm 的区别](#3. rm 和 git rm 的区别)
- 七、常用撤销命令和风险提醒
-
- [1. 回退版本](#1. 回退版本)
- [2. 找回历史操作](#2. 找回历史操作)
- [3. 丢弃工作区修改](#3. 丢弃工作区修改)
- [4. 撤销暂存区修改](#4. 撤销暂存区修改)
- [5. 删除文件](#5. 删除文件)
- [6. reset 三种模式对照](#6. reset 三种模式对照)
- 总结
Git 版本回退与撤销:写错代码后如何优雅反悔
前面已经整理了 Git 的本地仓库、工作区、暂存区、版本库,以及 git add、git commit、git status、git diff 等基础命令。
从这一篇开始,进入 Git 非常重要的一类能力:反悔能力。
写代码不可能永远一次写对,实际开发中经常会遇到这些情况:
- 某次提交写错了,想回到上一个版本;
- 工作区改乱了,想丢掉当前修改;
- 已经
git add了,但又不想提交这部分内容; - 已经
commit了,想撤回这次提交; - 文件误删了,想恢复;
- 文件确实不要了,想从版本库里删除。
这些场景都和 Git 的版本管理、撤销修改有关。
这一篇重点整理:
git reset如何回退版本;--soft、--mixed、--hard有什么区别;HEAD、HEAD^、HEAD~n怎么理解;git reflog如何找回历史操作;- 工作区、暂存区、版本库里的修改分别怎么撤销;
- 删除文件后如何恢复或彻底删除。
一、为什么需要版本回退
Git 最重要的能力之一,就是可以记录项目历史。
既然能记录历史,就意味着我们可以在需要的时候回到某个历史版本。
比如一个文件 ReadMe 经过了多次提交:
text
add version1
add version2
add version3
此时如果发现 version3 写错了,想回到 version2,就需要用到 版本回退。
Git 中用于版本回退的核心命令是:
bash
git reset
它的基本语法可以写成:
bash
git reset [--soft | --mixed | --hard] [目标版本]
其中,目标版本 可以是:
- 某个完整的
commit id; - 某个简短的
commit id; HEAD;HEAD^;HEAD~n。
不过,git reset 并不是简单地"把代码变回去"这么一句话就能解释完。
因为 Git 中有三个区域:
- 工作区
- 暂存区
- 版本库
不同参数会影响不同区域。
所以理解 git reset 时,关键不是只记命令,而是要想清楚:
这次回退到底要影响版本库、暂存区,还是工作区?

二、git reset 的三种模式
git reset 最常见的三个参数是:
--soft--mixed--hard
这三个参数的区别非常重要。
1. --soft:只移动版本库指针
--soft 只会移动当前分支指针,也就是让版本库回到指定提交。
它不会修改暂存区,也不会修改工作区。
可以理解为:
text
版本库回退
暂存区不变
工作区不变
示意:
bash
git reset --soft HEAD^
这种方式常用于:
刚提交完发现提交说明写错了,或者想把上一次提交拆开重新提交。
因为 --soft 不会丢掉修改,只是把提交撤回来,修改仍然保留在暂存区。
2. --mixed:回退版本库和暂存区
--mixed 是 git reset 的默认模式。
也就是说,下面两条命令效果类似:
bash
git reset HEAD^
git reset --mixed HEAD^
--mixed 会移动版本库指针,同时重置暂存区,但不会修改工作区。
可以理解为:
text
版本库回退
暂存区回退
工作区不变
这种方式常用于:
已经 git add 了,但想把暂存区里的内容撤出来,重新选择要提交的文件。
也就是说,--mixed 可以把修改从暂存区退回工作区。
3. --hard:版本库、暂存区、工作区全部回退
--hard 是最彻底,也最危险的模式。
它会同时回退:
- 版本库;
- 暂存区;
- 工作区。
可以理解为:
text
版本库回退
暂存区回退
工作区也回退
示例:
bash
git reset --hard HEAD^
执行后,工作区中的文件内容也会变成目标版本的样子。
这意味着:
如果工作区里有还没提交的修改,使用 --hard 可能会直接丢掉这些修改。
所以 --hard 一定要慎用。
在真实开发中,执行下面这种命令前最好先确认:
bash
git status
git diff
确认没有重要修改后,再考虑使用:
bash
git reset --hard 目标版本

三、HEAD、commit id 和版本定位
回退版本时,必须告诉 Git 要回到哪里。
Git 中常见的版本定位方式有几种。
1. 使用 commit id
每一次提交都有一个唯一的 commit id。
可以通过:
bash
git log --pretty=oneline
查看提交历史:
bash
d95c13ffc878a55a25a3d04e22abfc7d2e3e1383 (HEAD -> master) add version3
14c12c32464d6ead7159f5c24e786ce450c899dd add version2
cff9d1e019333318156f8c7d356a78c9e49a6e7b add version1
如果想回到 version2,可以直接指定对应的提交 ID:
bash
git reset --hard 14c12c32464d6ead7159f5c24e786ce450c899dd
Git 的 commit id 很长,但实际使用时通常不需要写完整。
只要前几位能唯一定位这次提交,就可以使用短 ID:
bash
git reset --hard 14c12c3
2. 使用 HEAD
HEAD 可以先简单理解为:
当前所在版本。
如果当前最新提交是 version3,那么 HEAD 就指向 version3。
常见写法:
bash
HEAD
表示当前版本。
bash
HEAD^
表示上一个版本。
bash
HEAD^^
表示上上一个版本。
不过在实际使用中,更推荐使用 HEAD~n 来表达往前数几个版本。
比如:
bash
HEAD~0
表示当前版本。
bash
HEAD~1
表示上一个版本。
bash
HEAD~2
表示上上一个版本。
这里补充一个容易混淆的点:
在线性提交历史中,HEAD^^ 可以理解为上上个版本,但 HEAD^2 并不等价于 HEAD~2。
HEAD^2 在 Git 中通常和合并提交的第二个父提交有关,后面学习分支合并时再展开。
所以普通版本回退时,想表示"往前退 n 个版本",建议优先使用:
bash
HEAD~n
比如回退到上一个版本:
bash
git reset --hard HEAD~1
回退到上上个版本:
bash
git reset --hard HEAD~2

四、实战:回退到旧版本,再找回新版本
下面用一个完整例子串起来。
假设 ReadMe 文件经历了三次提交:
bash
# 第一次提交
git add ReadMe
git commit -m "add version1"
# 第二次提交
git add ReadMe
git commit -m "add version2"
# 第三次提交
git add ReadMe
git commit -m "add version3"
查看提交历史:
bash
git log --pretty=oneline
输出类似:
bash
d95c13ffc878a55a25a3d04e22abfc7d2e3e1383 (HEAD -> master) add version3
14c12c32464d6ead7159f5c24e786ce450c899dd add version2
cff9d1e019333318156f8c7d356a78c9e49a6e7b add version1
当前最新版本是 version3。
1. 回退到 version2
如果发现 version3 写错了,希望工作区也回到 version2,可以使用:
bash
git reset --hard 14c12c32464d6ead7159f5c24e786ce450c899dd
或者使用短 ID:
bash
git reset --hard 14c12c3
执行后可能会看到:
bash
HEAD is now at 14c12c3 add version2
这说明当前版本已经回到了 version2。
此时查看文件内容:
bash
cat ReadMe
会发现 version3 对应的内容已经不在了。
再查看日志:
bash
git log --pretty=oneline
可能会看到:
bash
14c12c32464d6ead7159f5c24e786ce450c899dd (HEAD -> master) add version2
cff9d1e019333318156f8c7d356a78c9e49a6e7b add version1
此时 git log 中已经看不到 version3 了。
2. 回退之后又想回到 version3 怎么办
问题来了:如果回退到 version2 后,又后悔了,想重新回到 version3,怎么办?
如果还记得 version3 的 commit id,可以直接执行:
bash
git reset --hard d95c13f
但是实际开发中,终端输出可能早就被清掉了,git log 里也看不到 version3 了。
这时可以使用:
bash
git reflog
git reflog 会记录本地 HEAD 的移动历史。
示例输出:
bash
14c12c3 HEAD@{0}: reset: moving to 14c12c3
d95c13f HEAD@{1}: commit: add version3
14c12c3 HEAD@{2}: commit: add version2
cff9d1e HEAD@{3}: commit: add version1
这里可以看到 version3 的短提交 ID 是:
text
d95c13f
于是可以重新回到 version3:
bash
git reset --hard d95c13f
执行后:
bash
HEAD is now at d95c13f add version3
这样就找回来了。
3. git log 和 git reflog 的区别
git log 和 git reflog 都能看历史,但关注点不同。
可以简单理解为:
text
git log :查看当前提交链上的历史提交
git reflog :查看 HEAD 在本地移动过的历史记录
如果一次提交已经不在当前提交链上,git log 可能看不到。
但只要本地操作记录还在,git reflog 仍然有机会找到它。
所以,当你回退版本后发现"之前那个 commit 找不到了",不要慌,先试试:
bash
git reflog
五、撤销修改:按文件所在区域处理
撤销修改时,最重要的是先判断文件处在哪个区域。
同样是"撤销",文件状态不同,命令也不同。
常见情况有三种:
- 工作区修改了,但还没有
git add; - 已经
git add,但还没有git commit; - 已经
git add,也已经git commit。
不要一上来就乱敲 reset --hard。
更稳的思路是:
text
先 git status 看状态
再决定撤销工作区、暂存区,还是回退提交
1. 只修改了工作区,还没有 add
假设修改了 ReadMe:
bash
vim ReadMe
查看状态:
bash
git status
看到:
bash
Changes not staged for commit:
modified: ReadMe
这表示修改还停留在工作区,没有进入暂存区。
如果想丢弃工作区修改,可以使用:
bash
git checkout -- ReadMe
执行后,ReadMe 会恢复到最近一次 git add 或 git commit 时的状态。
这里有一个非常重要的点:
git checkout -- 文件名 中的 -- 不要省略。
因为省略后,checkout 可能会被 Git 理解成切换分支等其他含义。
现在新版本 Git 更推荐使用:
bash
git restore ReadMe
它的语义更直观:恢复工作区文件。
所以这两个命令可以对应理解:
bash
git checkout -- ReadMe
git restore ReadMe
它们都可以用于丢弃工作区修改。
不过如果你是跟着传统 Git 命令学习,知道 git checkout -- file 仍然很有必要,因为很多老项目和资料里都会看到它。
2. 已经 add,但还没有 commit
如果修改了 ReadMe,并且已经执行:
bash
git add ReadMe
此时再查看状态:
bash
git status
可能会看到:
bash
Changes to be committed:
modified: ReadMe
这说明修改已经进入暂存区。
如果想撤销暂存区,可以使用:
bash
git reset HEAD ReadMe
这个命令的作用是:
把 ReadMe 从暂存区撤出来,但工作区里的修改仍然保留。
执行后可能会看到:
bash
Unstaged changes after reset:
M ReadMe
这时再执行:
bash
git status
会发现文件又变成:
bash
Changes not staged for commit:
modified: ReadMe
也就是说,修改从暂存区退回到了工作区。
如果连工作区修改也不想要了,再执行:
bash
git checkout -- ReadMe
或者:
bash
git restore ReadMe
完整流程可以理解为:
bash
# 先撤销暂存区
git reset HEAD ReadMe
# 再丢弃工作区修改
git checkout -- ReadMe
新版本 Git 中,也可以使用:
bash
git restore --staged ReadMe
git restore ReadMe
其中:
bash
git restore --staged ReadMe
表示把文件从暂存区撤出来。
bash
git restore ReadMe
表示丢弃工作区修改。
3. 已经 commit
如果修改已经提交了,比如:
bash
git add ReadMe
git commit -m "bad change"
此时想撤销这次提交,就属于版本回退问题。
如果这个提交还没有推送到远程,可以使用:
bash
git reset --hard HEAD^
或者更清晰一点:
bash
git reset --hard HEAD~1
它表示回到上一次提交。
但是这里要非常小心:
如果这次提交已经推送到远程仓库,并且别人可能已经基于它继续开发,就不要随便用 reset --hard 改写历史。
在团队协作中,如果要撤销已经推送的提交,通常更推荐使用:
bash
git revert
git revert 会生成一次新的提交,用新的提交来抵消旧提交,而不是直接改写提交历史。
git revert 后面讲远程协作时再展开。
现在先记住:
- 本地未推送的错误提交,可以考虑
git reset; - 已推送、已共享的提交,不要轻易
reset --hard; - 团队协作中撤销公开提交,更推荐
git revert。
六、删除文件:恢复误删与彻底删除
在 Git 中,删除文件本身也是一种修改。
比如当前目录中有:
bash
file1 file2 file3 file4 file5 ReadMe
如果执行:
bash
rm file5
这只是从工作区删除了 file5。
此时查看状态:
bash
git status
会看到类似:
bash
Changes not staged for commit:
deleted: file5
这说明 Git 发现 file5 被删除了,但这个删除还没有提交到版本库。
此时一般有两种情况:
- 文件是误删的,想恢复;
- 文件确实不要了,想从版本库中删除。
1. 文件误删:恢复回来
如果 file5 是误删,可以使用:
bash
git checkout -- file5
或者新版本 Git:
bash
git restore file5
这样 Git 会从版本库中把 file5 恢复到工作区。
恢复后可以查看:
bash
ls
如果 file5 重新出现,就说明恢复成功。
这里的原理是:
版本库里还有 file5 的历史记录,所以可以恢复工作区中的误删文件。
2. 文件确实不要了:从版本库删除
如果 file5 确实不要了,只执行:
bash
rm file5
还不够。
因为这只是删除了工作区文件,还没有把"删除这个动作"提交到版本库。
更标准的方式是:
bash
git rm file5
然后提交:
bash
git commit -m "delete file5"
完整流程:
bash
git rm file5
git commit -m "delete file5"
提交成功后,可能会看到:
bash
delete mode 100644 file5
这说明 file5 已经从版本库记录中被删除。
当然,这并不表示 Git 历史里完全找不到 file5 了。
因为 Git 的历史提交仍然保存着旧版本。如果以后回到文件还存在的那个提交,依然可以看到它。
3. rm 和 git rm 的区别
可以这样理解:
text
rm file :只删除工作区文件
git rm file :删除工作区文件,并把删除动作加入暂存区
如果已经手动 rm file5 了,也可以再执行:
bash
git add file5
或者:
bash
git rm file5
把删除动作加入暂存区。
不过日常使用时,如果确定要删除 Git 管理的文件,直接使用:
bash
git rm file5
会更清晰。
七、常用撤销命令和风险提醒
这一篇涉及的命令比较多,最后做一个集中整理。
1. 回退版本
回退到指定提交:
bash
git reset --hard commit_id
回退到上一个版本:
bash
git reset --hard HEAD^
或者:
bash
git reset --hard HEAD~1
2. 找回历史操作
查看 HEAD 的移动记录:
bash
git reflog
找到目标提交后,可以回去:
bash
git reset --hard commit_id
3. 丢弃工作区修改
传统写法:
bash
git checkout -- file
新写法:
bash
git restore file
4. 撤销暂存区修改
传统写法:
bash
git reset HEAD file
新写法:
bash
git restore --staged file
5. 删除文件
确认删除:
bash
git rm file
git commit -m "delete file"
误删恢复:
bash
git checkout -- file
或者:
bash
git restore file
6. reset 三种模式对照
可以用下面这个表简单记忆:
| 命令 | 版本库 | 暂存区 | 工作区 | 常见用途 |
|---|---|---|---|---|
git reset --soft |
回退 | 不变 | 不变 | 撤回提交,保留暂存 |
git reset --mixed |
回退 | 回退 | 不变 | 取消暂存,保留修改 |
git reset --hard |
回退 | 回退 | 回退 | 彻底回到指定版本 |
其中最需要小心的是:
bash
git reset --hard
因为它会影响工作区。
所以使用前一定要确认:
bash
git status
git diff
如果工作区有重要修改,先提交,或者至少先备份,不要直接 --hard。

总结
这一篇主要整理了 Git 中的版本回退、撤销修改和删除文件。
核心内容包括:
git reset可以让仓库回到指定版本;--soft、--mixed、--hard影响的区域不同;HEAD表示当前版本;HEAD^表示上一个版本;HEAD~n更适合表示往前回退 n 个版本;commit id可以精确定位某次提交;git reflog可以查看本地HEAD移动记录;- 工作区修改可以用
git checkout -- file或git restore file丢弃; - 暂存区修改可以用
git reset HEAD file或git restore --staged file撤销; - 已提交但未推送的错误提交,可以考虑用
git reset回退; - 已经推送到远程并被别人使用的提交,不要随便
reset --hard; - 删除文件后,如果是误删,可以恢复;
- 如果确认删除,要使用
git rm并提交删除动作。
这篇最重要的理解是:
撤销之前先判断文件处在哪个区域。
工作区、暂存区、版本库对应的撤销方式不同。
只要区域判断清楚,Git 的"反悔操作"就不会乱。
下一篇会进入 Git 的分支基础,重点整理:
- 分支到底是什么;
HEAD和分支的关系;- 如何创建、切换、合并、删除分支;
- 什么是 Fast-forward;
- 为什么会产生合并冲突;
- 如何手动解决冲突。