四、Git 版本回退与撤销

文章目录

  • [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 addgit commitgit statusgit diff 等基础命令。

从这一篇开始,进入 Git 非常重要的一类能力:反悔能力

写代码不可能永远一次写对,实际开发中经常会遇到这些情况:

  • 某次提交写错了,想回到上一个版本;
  • 工作区改乱了,想丢掉当前修改;
  • 已经 git add 了,但又不想提交这部分内容;
  • 已经 commit 了,想撤回这次提交;
  • 文件误删了,想恢复;
  • 文件确实不要了,想从版本库里删除。

这些场景都和 Git 的版本管理、撤销修改有关。

这一篇重点整理:

  • git reset 如何回退版本;
  • --soft--mixed--hard 有什么区别;
  • HEADHEAD^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:回退版本库和暂存区

--mixedgit 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,怎么办?

如果还记得 version3commit 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 loggit 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 addgit 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 -- filegit restore file 丢弃;
  • 暂存区修改可以用 git reset HEAD filegit restore --staged file 撤销;
  • 已提交但未推送的错误提交,可以考虑用 git reset 回退;
  • 已经推送到远程并被别人使用的提交,不要随便 reset --hard
  • 删除文件后,如果是误删,可以恢复;
  • 如果确认删除,要使用 git rm 并提交删除动作。

这篇最重要的理解是:

撤销之前先判断文件处在哪个区域。

工作区、暂存区、版本库对应的撤销方式不同。

只要区域判断清楚,Git 的"反悔操作"就不会乱。

下一篇会进入 Git 的分支基础,重点整理:

  • 分支到底是什么;
  • HEAD 和分支的关系;
  • 如何创建、切换、合并、删除分支;
  • 什么是 Fast-forward;
  • 为什么会产生合并冲突;
  • 如何手动解决冲突。
相关推荐
xlq223222 小时前
5.git
git
努力努力再努力wz3 小时前
【Qt入门系列】:按钮组件全解析:从 QAbstractButton 到快捷键事件、单选与复选机制
c语言·开发语言·数据结构·c++·git·qt·github
Coin_learning5 小时前
Git 小白教程:从安装上云端,一步不落。
git
.wsy.8 小时前
Git教程(安装+流程+常用命令)
linux·git·gitee·intellij-idea
一勺菠萝丶9 小时前
Git Tag 使用教程:如何打 Tag、切换 Tag、推送 Tag 和删除 Tag
大数据·git·elasticsearch
xuhaoyu_cpp_java10 小时前
Git学习(八)
经验分享·笔记·git·学习
Irissgwe12 小时前
二、Git 本地仓库:从 git init 到第一次提交
git·gitee·github
时光无声£12 小时前
git提交拆分问题以及分批推送
git
憧憬成为java架构高手的小白13 小时前
修改暴露在github的密钥等隐私操作
git·github