前言
"改错了怎么办?""add错了怎么办?""提交错了怎么办?"------这是Git初学者最常问的问题。撤销与恢复是版本控制最核心的价值之一,也是最容易出事的地方(撤销错了可能丢失工作成果)。
本篇将系统讲解 git checkout 和 git restore 两个命令,帮你应对各种"后悔"场景。其中 git restore 是Git 2.23版本引入的新命令,专门用来替代 git checkout 中容易混淆的"恢复文件"功能。
一、什么时候需要撤销
开发中常见的"后悔"场景:
| 场景 | 描述 | 解决命令 |
|---|---|---|
| 改错了文件 | 工作区改了一堆,想恢复原样 | git restore / git checkout |
| add错了 | 暂存了不该暂存的文件 | git restore --staged |
| 提交错了 | commit了不该commit的 | git reset HEAD~1 |
| 提交信息写错 | 想修改提交信息 | git commit --amend |
| 已经push了 | 远程已经有了错误提交 | git revert |
| 删错了文件 | 误删了文件,想恢复 | git restore --source |
| 想回到某个旧版本 | 想查看或恢复历史某个版本 | git checkout |
本篇重点讲前几种场景,git reset 和 git revert 将在后续篇章详解。
二、git checkout 的多面性
git checkout 是Git中功能最多、也最容易混淆的命令。它能做这些事:
- 切换分支 :
git checkout main - 创建并切换分支 :
git checkout -b feature - 恢复工作区文件 :
git checkout -- <file> - 切换到某个提交 :
git checkout <commit>(分离头指针) - 从某提交恢复文件 :
git checkout <commit> -- <file>
问题在于:切换分支 和恢复文件用同一个命令,语义完全不同,初学者很容易搞混。比如:
git checkout main # 切换到main分支
git checkout -- main.js # 恢复main.js文件(--分隔)
如果没有 --,git checkout main 到底是分支还是文件?Git会优先当作分支名,但如果恰好有个文件叫 main,就会产生歧义。
正因为这种混淆,Git 2.23引入了两个新命令:
git switch:专门切换分支git restore:专门恢复文件
本篇重点讲 git restore,同时保留 git checkout 的讲解(因为大量老项目和教程仍在使用)。
三、恢复工作区文件
3.1 git checkout -- :恢复单个文件到暂存区状态
场景:你修改了 app.js,改了一堆发现不对,想恢复到修改前的状态。
# 修改了app.js
echo "错误的修改" >> src/app.js
# 查看状态
git status
输出:
修改尚未加入提交:
修改: src/app.js
恢复:
# 恢复app.js到暂存区的状态(即上次git add时的状态)
git checkout -- src/app.js
执行后 src/app.js 回到了暂存区的版本,刚才的修改丢失了。
3.2 git checkout -- .:恢复所有文件
# 恢复当前目录下所有被修改的文件
git checkout -- .
# 恢复整个项目
git checkout -- *
警告:这会丢弃所有未暂存的修改,无法恢复!
3.3 注意:会丢失未保存的修改!
git checkout -- <file> 是危险操作,执行后工作区的修改直接消失,无法通过Git找回(因为还没add,不在版本库里)。
安全做法 :如果不确定要不要丢弃,先用 git diff 看看改了什么:
git diff src/app.js # 先看看改了什么
git stash # 或者先stash保存起来
git checkout -- src/app.js # 确认后再恢复
四、git restore(Git 2.23+ 新命令)
git restore 专门用于恢复文件,语义清晰,推荐使用。
4.1 git restore :恢复工作区文件
等价于 git checkout -- <file>:
# 恢复app.js到暂存区的状态
git restore src/app.js
# 恢复多个文件
git restore src/app.js src/router.js
# 恢复所有文件
git restore .
4.2 git restore --staged :取消暂存
场景:git add 了一个不该add的文件,想把它从暂存区撤回工作区(不丢失修改)。
# 误add了
git add src/config.js
# 取消暂存(修改保留在工作区)
git restore --staged src/config.js
这等价于老命令 git reset HEAD src/config.js。
查看效果:
git add src/config.js
git status
输出:
要提交的变更:
修改: src/config.js
git restore --staged src/config.js
git status
输出:
修改尚未加入提交:
修改: src/config.js
文件修改还在,只是从暂存区撤回了工作区。
4.3 git restore --source= :恢复到指定提交的版本
场景:你想把某个文件恢复到某个历史提交时的状态。
# 恢复app.js到上一次提交时的状态
git restore --source=HEAD~1 src/app.js
# 恢复到指定commit
git restore --source=8f3a2d1 src/app.js
# 恢复到某个tag
git restore --source=v1.0 src/app.js
4.4 git restore --staged --source=HEAD :同时取消暂存并恢复
# 既取消暂存,又恢复工作区文件到HEAD状态
git restore --staged --source=HEAD src/app.js
这相当于:
git restore --staged src/app.js # 先取消暂存
git restore src/app.js # 再恢复工作区
4.5 --worktree 和 --staged 选项
git restore 默认操作工作区(--worktree),可以显式指定:
# 只恢复工作区(默认)
git restore --worktree src/app.js
# 只取消暂存
git restore --staged src/app.js
# 两者都做
git restore --worktree --staged src/app.js
五、各种撤销场景
5.1 场景1:修改了文件还没add
症状:工作区改了一堆,想丢弃修改。
# 当前状态
echo "不要的修改" >> src/app.js
git status
输出:
修改尚未加入提交:
修改: src/app.js
解决:
# 方法1:用restore(推荐)
git restore src/app.js
# 方法2:用checkout
git checkout -- src/app.js
验证:
git status
# 无文件要提交,干净的工作区
5.2 场景2:add了但还没commit
症状:暂存了文件,但不想提交了。
git add src/app.js
git status
输出:
要提交的变更:
修改: src/app.js
解决:
# 方法1:用restore(推荐)
git restore --staged src/app.js
# 方法2:用reset
git reset HEAD src/app.js
验证:
git status
输出:
修改尚未加入提交:
修改: src/app.js
修改还在工作区,只是从暂存区撤回了。
如果想连工作区的修改也丢弃:
git restore --staged src/app.js # 先取消暂存
git restore src/app.js # 再恢复工作区
# 或一步到位
git restore --staged --worktree src/app.js
5.3 场景3:commit了但还没push
症状:提交了,但发现提交错了,想撤销提交(保留修改)。
git commit -m "错误的提交"
解决(下篇详讲,这里先给出命令):
# 撤销提交,保留修改在暂存区
git reset --soft HEAD~1
# 撤销提交,保留修改在工作区
git reset --mixed HEAD~1 # 默认mixed
git reset HEAD~1
# 撤销提交,丢弃修改(危险!)
git reset --hard HEAD~1
5.4 场景4:已经push了
症状:提交并推送了,发现错了。
解决 :用 git revert 创建一个"反向提交"(后面详讲):
# 撤销最近一次提交(创建一个新提交来抵消它)
git revert HEAD
# 撤销指定提交
git revert 8f3a2d1
不要用 git reset --hard + git push --force,这会改写历史,影响其他协作者。
5.5 场景对比表
| 场景 | 命令 | 修改是否保留 |
|---|---|---|
| 改了没add | git restore | 丢弃 |
| add了没commit | git restore --staged | 保留在工作区 |
| commit了没push | git reset --soft HEAD~1 | 保留在暂存区 |
| commit了没push | git reset HEAD~1 | 保留在工作区 |
| commit了没push | git reset --hard HEAD~1 | 丢弃 |
| 已经push | git revert HEAD | 创建反向提交 |
六、git checkout vs git restore vs git reset 对比表
| 功能 | git checkout | git restore | git reset |
|---|---|---|---|
| 切换分支 | git checkout | ❌(用git switch) | ❌ |
| 创建分支 | git checkout -b | ❌(用git switch -c) | ❌ |
| 恢复工作区文件 | git checkout -- | git restore | ❌ |
| 取消暂存 | ❌ | git restore --staged | git reset HEAD |
| 恢复到指定提交 | git checkout -- | git restore --source | ❌ |
| 移动HEAD指针 | git checkout | ❌ | git reset |
| 重置到某提交(丢弃) | ❌ | ❌ | git reset --hard |
记忆要点:
git switch:切换分支(新)git restore:恢复文件(新)git reset:移动HEAD、重置暂存区git checkout:以上都能做(老,易混淆)
七、恢复删除的文件
7.1 误删了工作区的文件
# 误删
rm src/config.js
git status
输出:
修改尚未加入提交:
删除: src/config.js
恢复:
# 方法1:用restore
git restore src/config.js
# 方法2:用checkout
git checkout -- src/config.js
文件回来了!因为暂存区(或HEAD)里还有这个文件。
7.2 用git rm删除了并暂存
git rm src/config.js
git status
输出:
要提交的变更:
删除: src/config.js
恢复:
# 先取消暂存
git restore --staged src/config.js
# 再恢复文件
git restore src/config.js
# 或用reset
git reset HEAD src/config.js
git checkout -- src/config.js
7.3 已经commit了删除操作
git rm src/config.js
git commit -m "删除config.js"
恢复:
# 方法1:从上一次提交恢复
git checkout HEAD~1 -- src/config.js
git restore --source=HEAD~1 src/config.js
git add src/config.js
git commit -m "恢复config.js"
# 方法2:用revert撤销删除提交
git revert HEAD
八、从历史提交中恢复某个文件
8.1 实战场景
场景:不小心删了 config.js,而且已经提交了,3天后才发现。想从3天前的提交恢复这个文件。
步骤1:找到包含该文件的提交
# 查看该文件的历史
git log --oneline -- src/config.js
输出:
8f3a2d1 删除config.js
2b5c8e9 修改config配置
a1b2c3d 添加config.js
2b5c8e9 是删除前最后一次修改该文件的提交。
步骤2:恢复该文件
# 方法1:用checkout
git checkout 2b5c8e9 -- src/config.js
# 方法2:用restore
git restore --source=2b5c8e9 src/config.js
此时文件已恢复到工作区并自动暂存。
步骤3:提交
git status
输出:
要提交的变更:
新文件: src/config.js
git commit -m "恢复误删的config.js"
8.2 恢复某个文件的旧版本(不覆盖当前)
如果你只是想查看某文件的历史版本,而不想覆盖当前文件:
# 查看某提交时该文件的内容
git show 2b5c8e9:src/config.js
# 导出到文件
git show 2b5c8e9:src/config.js > config_old.js
8.3 恢复整个项目到某个历史版本
# 查看某个历史版本(分离头指针,只读)
git checkout 2b5c8e9
# 看完后回到最新
git checkout main
# 如果想把当前分支重置到那个版本(丢弃之后的提交)
git reset --hard 2b5c8e9
8.4 恢复某个目录下的所有文件
# 恢复src/pages目录下所有文件到某提交的状态
git checkout 2b5c8e9 -- src/pages/
git restore --source=2b5c8e9 -- src/pages/
8.5 交互式恢复部分修改
如果你只想恢复文件的部分修改(而不是整个文件),可以用交互式模式:
# 交互式选择要恢复的代码块
git checkout -p src/app.js
# 或
git restore -p src/app.js
Git会逐块显示diff,让你选择:
diff --git a/src/app.js b/src/app.js
@@ -10,3 +10,4 @@
init();
run();
+ console.log('debug');
(1/1) 应用此改动到工作区?[y,n,q,a,d,e,?]?
选项含义:
y:是,恢复这个hunkn:否,跳过q:退出,不再处理a:应用本文件所有hunkd:跳过本文件所有hunke:手动编辑这个hunk?:帮助
这在"改了一堆但只想丢弃其中一部分"时非常有用。
九、注意事项
9.1 撤销是危险的
未提交的修改,一旦撤销,无法通过Git找回。
git restore src/app.js # 工作区修改丢失,找不回
git checkout -- src/app.js # 同上
git reset --hard HEAD~1 # 提交和修改都丢失,找不回
这些命令执行后,修改不在版本库里,git reflog 也救不了(reflog只记录HEAD的移动,不记录工作区内容)。
安全习惯:
- 撤销前先
git status和git diff看清楚 - 不确定就先
git stash保存 - 重要修改先commit一次(哪怕信息写"临时保存")
9.2 git stash:安全的中转
# 暂存当前修改
git stash
# 查看stash列表
git stash list
# 恢复stash
git stash pop
# 恢复指定stash
git stash apply stash@{1}
# 丢弃stash
git stash drop
git stash 是撤销前的"保险绳"。
9.3 已推送的提交不要reset
# 错误做法:重写已推送的历史
git reset --hard HEAD~1
git push --force # 危险!可能覆盖别人的工作
正确做法:用 git revert 创建反向提交。
9.4 amend不要用于已推送的提交
git commit --amend -m "新信息"
git push --force # 同样危险
十、常见问题 FAQ
Q1:git restore 和 git checkout -- 有什么区别?
功能上等价,git restore 是Git 2.23+的新命令,语义更清晰,推荐使用。老版本Git只能用 git checkout --。
Q2:恢复文件后为什么自动暂存了?
git checkout <commit> -- <file> 和 git restore --source=<commit> <file> 会把文件恢复到工作区并暂存。如果不想要暂存:
git restore --source=<commit> --staged --worktree <file>
# 或恢复后取消暂存
git restore --staged <file>
Q3:git restore --staged 和 git reset HEAD 一样吗?
是的,都是把文件从暂存区撤回工作区。git restore --staged 是新命令,更直观。
Q4:如何撤销git restore?
如果 git restore 丢弃了工作区修改,无法直接撤销。但如果之前有stash或commit,可以从中恢复。
Q5:git checkout . 和 git restore . 一样吗?
基本一样,都是恢复当前目录所有文件。但 git checkout . 在某些情况下可能行为不同(如遇到分支名和文件名冲突),git restore . 更明确。
Q6:如何只恢复文件的一部分修改?
Git不支持部分恢复,但可以:
- 恢复整个文件
- 然后手动重新做需要的修改
或用交互式add的反向操作:
git checkout -p # 交互式选择要恢复的hunk
Q7:restore --source=HEAD 和 不加source有什么区别?
git restore <file> # 恢复到暂存区的状态
git restore --source=HEAD <file> # 恢复到HEAD的状态
如果暂存区和HEAD不同,结果不同。通常没add时两者相同。
Q8:git switch 和 git checkout 切换分支有什么区别?
git switch 是Git 2.23+引入的,专门用于切换分支,语义更清晰:
# 切换分支
git switch main # 等价于 git checkout main
git switch -c feature # 等价于 git checkout -b feature
# 切换到上一个分支
git switch - # 等价于 git checkout -
git switch 不能用来恢复文件,避免了 git checkout 的语义混淆问题。
Q9:如何撤销git stash?
# 误stash了,想取回来
git stash pop
# 如果pop后发现有冲突,想撤销pop
# stash内容还在stash列表里(pop失败不会删除)
git stash list
git stash apply stash@{0}
Q10:如何查看某个文件在某个历史版本的内容?
不需要恢复,只想查看:
# 查看某提交时文件的内容
git show 2b5c8e9:src/config.js
# 查看某分支中文件的内容
git show feature:src/config.js
# 查看某个tag时文件的内容
git show v1.0:src/config.js
# 导出到文件
git show 2b5c8e9:src/config.js > old_config.js
十一、最佳实践
11.1 撤销前先看
git status # 看状态
git diff # 看改动内容
11.2 不确定就stash
git stash # 先存起来
# 尝试撤销操作
# 如果发现需要那些修改
git stash pop # 取回来
11.3 用新命令
Git 2.23+优先使用:
git switch <branch> # 切换分支
git restore <file> # 恢复文件
git restore --staged <file> # 取消暂存
比 git checkout 语义清晰,不易出错。
11.4 重要操作前先commit
哪怕只是"临时保存",也先commit一次:
git commit -m "WIP: 临时保存"
commit后的内容在版本库里,git reflog 能找回;未commit的工作区修改丢了就是丢了。
11.5 团队协作中慎用force push
# 危险
git push --force
# 相对安全(如果远程有别人的提交会拒绝)
git push --force-with-lease
十二、小结
本篇详细讲解了撤销与恢复的两大命令:
| 知识点 | 要点 |
|---|---|
| git checkout -- | 恢复工作区文件(老命令) |
| git restore | 恢复工作区文件(新命令,推荐) |
| git restore --staged | 取消暂存(从暂存区撤回工作区) |
| git restore --source | 恢复到指定提交的版本 |
| git checkout -- | 从历史提交恢复文件 |
| 场景1:改了没add | git restore |
| 场景2:add了没commit | git restore --staged |
| 场景3:commit了没push | git reset HEAD~1(下篇详讲) |
| 场景4:已经push | git revert(后续详讲) |
| 注意事项 | 未提交的修改撤销后无法恢复 |
核心记忆:
# 恢复工作区文件(丢弃修改)
git restore <file>
git checkout -- <file>
# 取消暂存(保留修改)
git restore --staged <file>
# 恢复到指定版本
git restore --source=<commit> <file>
git checkout <commit> -- <file>
# 切换分支(新命令)
git switch <branch>
git checkout <branch>
撤销场景速查:
改了没add → git restore <file>
add了没commit → git restore --staged <file>
commit了没push→ git reset HEAD~1
已经push → git revert HEAD
安全口诀:撤销前先看,不确定先stash,重要修改先commit,已推送的别reset。
下一篇我们将深入学习 git reset------理解 --soft、--mixed、--hard 三种模式的区别,彻底掌握HEAD指针的重置。
作者:全栈陈序员 出处:CSDN 声明:本文为原创文章,转载请注明出处