1. 问题场景
代码写着写着,改错了,或者发现方向偏了,需要把文件恢复到之前的状态。但错误的修改可能处于不同阶段:也许你只是改了工作区的文件还没 add,也许已经 add 到了暂存区,又或者已经执行了 commit。针对这三种情况,Git 提供了不同的撤销手段。关键不是记住所有命令,而是先判断你的修改处于哪个区域,再对症下药。
2. 情况一:工作区修改了,但还没 add
这是最轻微的状况,你改了文件,但没有执行过 git add。此时工作区的内容和版本库不一致,你想丢弃本地修改,回到最近一次 add 或 commit 之后的状态。
核心命令
bash
git checkout -- <file>
这条命令会直接把指定的文件恢复到暂存区或版本库中的状态。⚠️ 它会覆盖工作区的内容,操作不可逆,执行前确认你真的不要这些修改了。
实战演示
在仓库里故意往 ReadMe 中写入一行错误内容:
bash
$ echo "This is bad code" >> ReadMe
查看状态,Git 提示文件被修改:
bash
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: ReadMe
no changes added to commit (use "git add" and/or "git commit -a")
Git 已经给出了撤销的提示。现在执行:
bash
$ git checkout -- ReadMe
没有输出,意味着成功了。用 cat 确认内容已恢复:
bash
$ cat ReadMe
# 之前追加的那行 "This is bad code" 已经消失
再次执行 git status,工作区恢复干净。
3. 情况二:已经 add 了,但还没 commit
你不仅修改了文件,还执行了 git add,把改动放入了暂存区。此时相当于"准备提交"的状态。想撤销,需要分两步:先把文件从暂存区撤出来(回到情况一),再丢弃工作区的修改。
核心命令
第一步,把文件从暂存区拉回工作区:
bash
git reset HEAD <file>
这里的 HEAD 可以理解为最新版本库的快照。这条命令让暂存区的内容与 HEAD 保持一致,但工作区的文件内容不变。此时文件就变成了"已修改但未暂存"的状态(即情况一)。
第二步,再丢弃工作区的修改:
bash
git checkout -- <file>
实战演示
还是以 ReadMe 为例,写入一行错误内容并 add:
bash
$ echo "This is bad code" >> ReadMe
$ git add ReadMe
此时查看状态:
bash
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: ReadMe
Git 提示可以使用 git reset HEAD <file> 来取消暂存。执行:
bash
$ git reset HEAD ReadMe
Unstaged changes after reset:
M ReadMe
输出告诉我们,ReadMe 已经不在暂存区了,工作区的修改仍然保留。再看状态,又回到了情况一:
bash
$ git status
Changes not staged for commit:
modified: ReadMe
最后一步,丢弃工作区修改:
bash
$ git checkout -- ReadMe
文件恢复,工作区干净。
补充说明
新版本的 Git(2.23 及以上)提供了更直观的命令 git restore,可以一步完成相同的操作:
- 从暂存区撤出到工作区:
git restore --staged <file> - 丢弃工作区修改:
git restore <file>
如果你用的是较新的 Git 版本,可以用这两个命令代替上面的两步操作。但 git reset HEAD 和 git checkout -- 的写法更通用,几乎所有环境都支持。
4. 情况三:已经 commit 了(但没推送到远程)
当你连 commit 都执行了,说明改动已经进入了版本库。要撤销这次提交,需要移动 HEAD 指针,也就是执行版本回退。前提是这次提交还没有 push 到远程仓库;如果已经推送,情况会复杂得多,这里暂不展开。
核心命令
bash
git reset --hard HEAD^
HEAD^ 表示当前提交的上一级,也就是回退一个版本。--hard 参数会让工作区和暂存区都同步回退。
如果你想撤销最后一次提交,但保留工作区里的改动(比如想重新修改后再提交),可以使用 --soft 或 --mixed,具体选择取决于你希望暂存区是什么状态。多数场景下,想彻底撤销,用 --hard;想留着改动继续改,用 --mixed(默认)或 --soft。上一篇文章已经详细对比过三种模式,此处不再重复。
实战演示
假设你在仓库里刚刚提交了一个错误版本:
bash
$ echo "bad version" >> ReadMe
$ git add ReadMe
$ git commit -m "a bad commit"
[master e3f4a5b] a bad commit
1 file changed, 1 insertion(+)
现在想撤销这次提交,直接回到上一个版本:
bash
$ git reset --hard HEAD^
HEAD is now at 14c12c3 add version2
git log 里已经看不到 a bad commit 那条记录了,工作区的文件也恢复到了之前的状态。
⚠️ 如果这次提交已经推送到了远程仓库,执行 reset 只是改了你本地的历史,远程仓库的版本并没有变。这时如果你再 push,会被拒绝,因为历史分叉了。这种场景下的正确处理方式是使用 git revert 创建一个反向提交,而不是直接 reset 历史。多人协作中尤其要注意这一点。
5. 核心思想
无论哪种情况,Git 都会给你留退路。重要的是先冷静下来,用 git status 明确当前文件处在哪个区域(工作区、暂存区、版本库),然后选择对应策略:
- 工作区乱了 →
git checkout -- <file>(或git restore <file>) - 暂存区乱了 →
git reset HEAD <file>先撤到工作区,再用上一条命令丢弃修改 - 版本库乱了 →
git reset --hard HEAD^回退提交
6. 注意事项
git checkout -- <file>和git reset --hard都会直接覆盖文件,是无法通过普通 Git 操作恢复的。执行前务必确认不需要这些修改,或者用git stash先暂存一下(后续文章会讲)。- 如果你不确定当前状态,永远先跑一次
git status,Git 的提示信息会告诉你下一步该怎么做。 - 对于已经
push到远程的提交,尽量避免reset,改用revert来保证公共历史的完整。
7. 要点总结
- 撤销修改分三种场景,核心在于判断修改处于工作区、暂存区还是版本库。
- 工作区未暂存:
git checkout -- <file>直接丢弃修改。 - 已暂存未提交:
git reset HEAD <file>取消暂存,再用checkout丢弃。 - 已提交未推送:
git reset --hard HEAD^回退整个版本。 git status是你在这些场景中最可靠的指南。
8. 练习题
- 在仓库中修改一个文件,处于"未暂存"状态,练习用
git checkout -- <file>撤销修改。 - 将文件
add到暂存区,练习用git reset HEAD <file>撤销暂存,再丢弃工作区修改。 - 进行一次提交,然后用
git reset --hard HEAD^彻底撤销这次提交。确认工作区也回到了之前的状态。 - (进阶)故意进行一次提交后,用
git reset --mixed HEAD^撤销,观察工作区和暂存区的状态,再重新提交。 - (思考题)如果你同时修改了多个文件,其中一些想撤销、另一些想保留,该如何分别操作?结合
git status的输出设计你的步骤。