在使用 Git 工作时,经常会遇到这样的情况:写了一段代码,或者对文件做了一些修改,但后来发现这些改动不好,想回到之前的状态。根据你的改动所处的“阶段”(在工作区、暂存区还是已经提交到了版本库),撤销修改的方法是不同的。
撤销修改:根据“修改在哪里”选择方法
Git 非常灵活,它允许你在不同的阶段撤销修改。理解这一点,就能帮助你选择正确的命令。
情况一:修改只在工作区(还没有 git add
)
场景: 你在工作区(你的项目文件夹里)修改了文件,或者新建了文件,但你还没有执行 git add
命令将这些改动添加到暂存区。此时 git status
会显示 “Changes not staged for commit”(未暂存的修改)。现在,你想丢弃这些在工作区里的改动,让文件回到你上次 git add
或 git commit
时的状态。
就像你提到的,如果只改了一两行,你当然可以手动删掉。但如果改了很多文件,或者改动非常复杂,手动删除费时费力,还容易出错,特别是你记不清到底改了哪些地方时。
这时,Git 为我们提供了更方便的“撤销”方法。
命令: git checkout -- [文件名]
或者使用 Git 较新版本推荐的命令(功能类似):git restore [文件名]
这里只讲解:
git checkout --
。
注意: 命令中的 --
非常重要,它是用来分隔 Git 选项和文件列表的。如果省略了 --
,git checkout
命令就变成了切换分支(我们后面会讲到),而不是撤销文件修改,效果完全不同!
它的原理: git checkout -- [文件名]
命令的意义是:“Git,请你用暂存区(如果文件在暂存区有改动)或版本库中当前分支最新提交(如果文件不在暂存区)的那个版本的文件,来覆盖我工作区的当前文件。”
简单来说,它会把工作区的这个文件“打回原形”,恢复到它最近一次被纳入 Git 快照时的状态。
操作演示:
- 我们在
ReadMe
文件中新增一行代码(例如 “This piece of code is like shit”)。
# 查看修改前的文件内容(这是上一次 commit 后的状态)
zz@139-159-150-152:~/gitcode$ cat ReadMe
hello bit
hello git
hello world
hello version1
hello version2
hello version3
# 手动编辑 ReadMe,在末尾新增一行
zz@139-159-150-152:~/gitcode$ vim ReadMe
# 编辑后 ReadMe 内容如下:
# ...
# hello version3
# This piece of code is like shit
# 使用 git status 查看,发现改动只在工作区,未暂存
zz@139-159-150-152:~/gitcode$ git status
On branch master
Changes not staged for commit: # 未暂存的修改
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: ReadMe # ReadMe 文件被修改了
no changes added to commit # 暂存区没有改动
- 现在我们想撤销工作区对
ReadMe
的修改。
# 执行撤销命令
zz@139-159-150-152:~/gitcode$ git checkout -- ReadMe
Git 不会给出太多输出,但它已经悄悄地用版本库中最新提交的 ReadMe
文件内容覆盖了你的工作区文件。
- 再次查看
ReadMe
内容和git status
状态:
# 查看 ReadMe 内容
zz@139-159-150-152:~/gitcode$ cat ReadMe
hello bit
hello git
hello world
hello version1
hello version2
hello version3
# 新增的那一行 "This piece of code is like shit" 不见了!
# 查看 Git 状态
zz@139-159-150-152:~/gitcode$ git status
On branch master
nothing to commit, working tree clean # 工作区又干净了
成功了!工作区的修改被撤销了。
记住: git checkout -- [文件名]
(或 git restore [文件名]
)命令可以丢弃工作区里未暂存的修改。它会用暂存区或版本库中的版本覆盖工作区的文件。
情况二:修改已添加到暂存区(已经 git add
,但没有 git commit
)
场景: 你在工作区做了修改,然后执行了 git add [文件名]
命令,将改动添加到了暂存区。此时 git status
会显示 “Changes to be committed”(待提交的修改)。但你突然发现 add
到暂存区的改动有问题,不应该提交,你想把这个改动从暂存区撤回到工作区(或者彻底丢弃)。
命令: git reset HEAD [文件名]
这里我们再次遇到了 git reset
命令!还记得我们讲版本回退时提到的 --mixed
参数吗?它是 git reset
的默认参数,作用是移动分支指针并重置暂存区,但不改变工作区。
git reset HEAD [文件名]
就是利用了 git reset --mixed
的原理,但它更加精确。
HEAD
指的是当前分支的最新提交。[文件名]
表示这个操作只针对指定的文件。- 因为
--mixed
是默认的,所以git reset HEAD [文件名]
等同于git reset --mixed HEAD [文件名]
。
它的原理: git reset HEAD [文件名]
命令的意义是:“Git,请你把暂存区里这个文件的状态,回退到 HEAD
指向的那个版本(也就是当前分支最新提交)时的状态。但不要改变我的工作区。”
执行这个命令后,暂存区里这个文件的改动就会被移除,回到它在最新提交时的状态。而你在工作区对这个文件的修改不会丢失,它们会回到“未暂存”(unstaged)的状态,就像刚修改完还没 add
那样。
操作演示:
- 我们在
ReadMe
中新增一行代码(和上面一样),然后将其add
到暂存区。
# 修改 ReadMe 文件,新增一行 "This piece of code is like shit"
zz@139-159-150-152:~/gitcode$ vim ReadMe
# Add 到暂存区
zz@139-159-150-152:~/gitcode$ git add ReadMe
# 查看状态,改动已在暂存区
zz@139-159-150-152:~/gitcode$ git status
On branch master
Changes to be committed: # 待提交的修改
(use "git restore --staged <file>..." to unstage) # Git 提示你可以用 restore --staged 撤销暂存,它和 git reset HEAD 功能类似
modified: ReadMe
- 现在我们想撤销暂存区的修改。
# 执行撤销暂存命令
zz@139-159-150-152:~/gitcode$ git reset HEAD ReadMe
Unstaged changes after reset: # Git 提示:重置后有未暂存的修改
M ReadMe # ReadMe 文件处于 Modified (修改) 状态且未暂存
- 再次查看
git status
状态:
zz@139-159-150-152:~/gitcode$ git status
On branch master
Changes not staged for commit: # 未暂存的修改
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: ReadMe # ReadMe 文件现在处于这个区域了!
no changes added to commit
成功了!ReadMe
的改动已经从暂存区回到了工作区,变成了未暂存的状态。
接下来怎么办? 现在你的修改回到了工作区(处于情况一),你可以选择:
- 继续编辑这个文件,直到满意为止。
- 如果想彻底丢弃这个修改,就像情况一那样,再次使用
git checkout -- ReadMe
来丢弃工作区的改动。
# 彻底丢弃工作区的修改
zz@139-159-150-152:~/gitcode$ git checkout -- ReadMe
# 检查状态和文件内容,一切都回到了上一次提交时的干净状态
zz@139-159-150-152:~/gitcode$ git status
On branch master
nothing to commit, working tree clean
zz@139-159-150-152:~/gitcode$ cat ReadMe
# 内容已经恢复到撤销修改之前
记住: git reset HEAD [文件名]
命令可以撤销暂存区里指定文件的修改,将改动回退到工作区,文件变为未暂存状态。
情况三:修改已提交到版本库(已经 git add
,并且 git commit
了)
场景: 你已经把修改通过 git commit
命令提交到了版本库,生成了一个新的版本。但提交后你突然发现,这个版本有问题,你想撤销这次提交,回到上一个版本。
命令: git reset --hard HEAD^
(或其他指定上一个版本的方式)
它的原理: 这就是我们上一篇讲的版本回退操作。git reset --hard HEAD^
命令的意义是:“Git,请你把当前分支的指针和 HEAD
指针,都移动到当前提交的上一个版本 (HEAD^
)。同时,强制性地把暂存区和工作区的内容都重置为上一个版本时的状态。”
这样一来,你做的最后一次提交就从当前分支的历史中“消失”了,你的文件内容也回到了上一个提交时的状态。
【重要提示】:就像上一篇强调的,这种方式会丢弃最后一次提交之后所有在工作区的修改(如果它们没有被提交的话)。而且,如果你已经把这个错误的提交推送到了远程仓库(后面我们会讲到远程仓库),这种 reset --hard
会修改共享的历史记录,给团队协作带来麻烦,通常不推荐这样做。只在本地仓库且确认没问题时使用。
操作演示:
- 我们在
ReadMe
文件中新增一行,然后add
并commit
这个改动。
# 确保工作区是干净的
zz@139-159-150-152:~/gitcode$ git status
On branch master
nothing to commit, working tree clean
# 修改 ReadMe 文件,新增一行 "This piece of code is like shit"
zz@139-159-150-152:~/gitcode$ vim ReadMe
# 内容变成:... hello version3\nThis piece of code is like shit
# Add 到暂存区
zz@139-159-150-152:~/gitcode$ git add ReadMe
# Commit 提交到版本库
zz@139-159-150-152:~/gitcode$ git commit -m"test quash" # 提交消息为 "test quash"
[master 5f71ae1] test quash # 产生了新的 commit id
1 file changed, 1 insertion(+)
# 查看历史,确认新的提交已存在,并且 HEAD 指向它
zz@139-159-150-152:~/gitcode$ git log --pretty=oneline
5f71ae1f3e2c7b1a8c9e1f0f6bff3015df71a0963004476f5e6cfd54 (HEAD -> master) test quash # 最新提交
d95c13ffc878a55a25a3d04e22abfc7d2e3e1383 add version3 # 上一个提交
...
- 现在我们想撤销这个
test quash
提交,回到add version3
那个版本。
# 执行版本回退命令,回到上一个版本,并强制重置工作区和暂存区
zz@139-159-150-152:~/gitcode$ git reset --hard HEAD^
HEAD is now at d95c13f add version3 # Git 告诉你 HEAD 回到了上一个提交
注意:这里的 HEAD^
就是指 5f71ae1
的父提交,也就是 d95c13f
。
- 检查工作区文件内容:
zz@139-159-150-152:~/gitcode$ cat ReadMe
hello bit
hello git
hello world
hello version1
hello version2
hello version3
# 新增的那一行 "This piece of code is like shit" 消失了!文件回到了 version3 提交时的状态。
- 检查提交历史:
zz@139-159-150-152:~/gitcode$ git log --pretty=oneline
d95c13ffc878a55a25a3d04e22abfc7d2e3e1383 (HEAD -> master) add version3 # 最新提交已经是它了
... # 之前的其他提交
那个 “test quash” 的提交在 git log
里看不到了,成功撤销了这次提交。
记住: git reset --hard HEAD^
(或其他 git reset --hard [目标版本]
) 可以撤销版本库中的一次或多次提交,并将暂存区和工作区都回退到目标版本。这是一个强大的回退命令,会丢失工作区未提交的修改,且不应用于已推送到远程的共享历史。
撤销修改命令速查表
场景 | 修改在哪儿? | git status 显示? |
想达到的效果? | 使用命令? | 对 各区域 的影响 | 注意 |
---|---|---|---|---|---|---|
想丢弃工作区的修改 | 工作区 | Changes not staged for commit |
工作区文件恢复到暂存区/版本库 | git checkout -- [文件名] |
工作区被覆盖,暂存区/版本库不变 | -- 很重要!会丢弃工作区未暂存的修改。 |
想取消暂存区的修改 | 暂存区 | Changes to be committed |
暂存区文件回退到工作区(未暂存) | git reset HEAD [文件名] |
暂存区被重置,工作区不变,版本库不变 | 实际上是 --mixed 模式的应用。修改回到工作区。 |
想撤销最近一次提交 | 版本库 | 工作区和暂存区通常是干净的 | 版本库回退到上一个版本,工作区/暂存区也回退 | git reset --hard HEAD^ |
版本库回退,暂存区/工作区强制重置 | 非常危险! 丢弃工作区未提交修改,不用于共享历史。 |
(额外) 想取消暂存所有改动 | 暂存区 | Changes to be committed |
暂存区所有文件回退到工作区 | git reset HEAD (不加文件名) |
暂存区被重置,工作区不变,版本库不变 | 常用,等同于 git reset --mixed HEAD 。 |
通过上面的讲解和速查表,你应该对如何在不同阶段撤销修改有了清晰的认识。选择正确的命令,理解它对工作区、暂存区和版本库的影响,是安全有效地使用 Git 的基础。记住 --hard
的强大和危险,并在必要时使用 git reflog
来找回历史记录!