Git 提供了丰富的撤销和恢复机制,可以应对从简单的"撤销刚才的编辑"到复杂的"找回丢失的提交"等各种场景。理解这些命令能让你在犯错时从容应对,不再害怕误操作。本文将系统介绍 Git 中所有与撤销、恢复相关的命令,每个命令都配有详细说明和实际示例。
一、核心概念与原理
| 概念 | 说明 |
|---|---|
| 工作区(Working Directory) | 你正在编辑的文件系统状态 |
| 暂存区(Staging Area / Index) | 下一次提交要保存的快照 |
| 版本库(Repository) | 存储所有提交对象的数据库 |
| HEAD | 指向当前分支最新提交的指针 |
| 引用(Refs) | 指向提交的指针(分支、标签、HEAD、reflog 等) |
| 可访问对象(Reachable) | 从某个引用(如分支)可以到达的提交 |
| 悬空对象(Dangling) | 没有任何引用指向的提交(可通过 reflog 或 fsck 找回) |
Git 的撤销哲学:
- Git 通常不会立即删除数据,而是让数据变得"不可达"
- 即使执行了
reset --hard或删除了分支,提交仍然存在于.git/objects中,直到被垃圾回收 reflog记录了 HEAD 和分支的所有移动,是恢复的重要工具
二、工作区撤销(未暂存的修改)
1. git restore -- 丢弃工作区的修改(推荐,Git 2.23+)
语法
bash
git restore <文件>...
git restore --source=<提交> <文件> # 从指定提交恢复文件
示例
bash
# 丢弃 README.md 的所有工作区修改(恢复到暂存区或 HEAD 的状态)
$ git restore README.md
# 丢弃当前目录下所有工作区修改
$ git restore .
# 从某个提交恢复文件到工作区(覆盖当前修改)
$ git restore --source=abc123 README.md
# 从上一个提交恢复文件
$ git restore --source=HEAD~1 src/app.js
# 交互式丢弃部分修改块(精细控制)
$ git restore -p README.md
# 会逐个显示修改块,询问是否丢弃
2. git checkout -- -- 丢弃工作区修改(传统方式,依然有效)
语法
bash
git checkout -- <文件>...
示例
bash
# 丢弃工作区修改
$ git checkout -- README.md
# 丢弃所有修改
$ git checkout -- .
3. git clean -- 删除未跟踪的文件和目录
语法
bash
git clean [选项]
常用选项
| 选项 | 说明 |
|---|---|
-n / --dry-run |
预览要删除的文件(安全) |
-f / --force |
强制删除(必须) |
-d |
同时删除未跟踪的目录 |
-x |
删除被 .gitignore 忽略的文件 |
-X |
只删除被 .gitignore 忽略的文件 |
-i / --interactive |
交互式删除 |
示例
bash
# 预览会删除哪些未跟踪文件
$ git clean -n
# 删除所有未跟踪文件(不包含目录)
$ git clean -f
# 删除未跟踪文件和目录
$ git clean -fd
# 删除包括被忽略的文件(如 .log, node_modules/)
$ git clean -fdx
# 只删除被 .gitignore 忽略的文件
$ git clean -fX
# 交互式删除(逐个确认)
$ git clean -i
三、暂存区撤销
4. git restore --staged -- 将文件移出暂存区(取消暂存)
语法
bash
git restore --staged <文件>...
示例
bash
# 取消暂存 README.md(保留工作区修改)
$ git restore --staged README.md
# 取消暂存所有文件
$ git restore --staged .
# 效果等同于旧的:git reset HEAD <file>
5. git reset (mixed) -- 重置暂存区到指定提交
语法
bash
git reset [--mixed] <提交> # --mixed 是默认模式
git reset HEAD <文件> # 取消暂存特定文件(旧语法)
示例
bash
# 取消所有暂存(将暂存区重置为 HEAD 的内容,工作区不变)
$ git reset
# 或
$ git reset HEAD
# 取消特定文件的暂存(旧语法)
$ git reset HEAD README.md
# 将暂存区重置为上一个提交的状态(撤销 add 和 commit)
$ git reset HEAD~1
# 此时修改保留在工作区,但暂存区已清空
6. git rm --cached -- 停止跟踪文件但保留工作区
语法
bash
git rm --cached <文件>...
示例
bash
# 将文件从 Git 跟踪中移除(但保留工作区),常用于修改 .gitignore 后
$ git rm --cached config.local.json
# 递归移除目录
$ git rm --cached -r logs/
四、提交撤销
7. git commit --amend -- 修改上一次提交
语法
bash
git commit --amend [-m "<新消息>"]
说明
- 用一个新的提交替换上一次提交(不增加提交数量)
- 可以修改提交消息、添加遗漏的文件、修改内容
示例
bash
# 修改上一次提交的消息
$ git commit --amend -m "修正的提交消息"
# 添加遗漏的文件到上一次提交
$ git add forgotten-file.txt
$ git commit --amend --no-edit # 保持原消息
# 修改上一次提交的内容(编辑文件后)
$ echo "new content" >> file.txt
$ git add file.txt
$ git commit --amend
# 完全重置上一次提交(相当于撤销 commit,保留修改)
$ git commit --amend --no-edit --reset-author # 重置作者信息
注意 :如果已经推送了提交,--amend 后需要 --force-with-lease 推送,且应避免在公共分支上这样做。
8. git reset -- 移动 HEAD 并可选重置暂存区/工作区
这是最强大的撤销命令,可以"回退"到任意历史状态。
三种模式
| 模式 | HEAD 指向 | 暂存区 | 工作区 | 典型用途 |
|---|---|---|---|---|
--soft |
改变 | 不变 | 不变 | 撤销提交,保留修改在暂存区 |
--mixed (默认) |
改变 | 重置到目标提交 | 不变 | 撤销提交和暂存,保留修改在工作区 |
--hard |
改变 | 重置到目标提交 | 重置到目标提交 | 彻底丢弃提交及所有修改(危险) |
示例
bash
# 撤销最后一次提交,保留修改在暂存区(相当于取消 commit)
$ git reset --soft HEAD~1
# 现在修改仍在暂存区,可直接再次 commit
# 撤销最后一次提交,并将修改移回工作区(取消 commit 和 add)
$ git reset HEAD~1 # 等价于 --mixed
# 撤销最近 3 次提交,保留修改在工作区
$ git reset --mixed HEAD~3
# 彻底丢弃最近 2 次提交的所有更改(不可恢复,除非有 reflog)
$ git reset --hard HEAD~2
# 将当前分支指向另一个提交(放弃当前分支之后的所有提交)
$ git reset --hard abc123
# 将分支指针移动到某个标签位置
$ git reset --hard v1.0
# 撤销合并(假设合并后未提交任何新内容)
$ git reset --hard ORIG_HEAD # ORIG_HEAD 保存了合并前的 HEAD
9. git revert -- 通过创建新提交来撤销某次提交(安全)
与 reset 不同,revert 不删除历史,而是生成一个反向的新提交。这是公共分支上推荐的撤销方式。
语法
bash
git revert <提交哈希>...
常用选项
| 选项 | 说明 |
|---|---|
-n / --no-commit |
只应用更改,不自动提交(可批量 revert) |
--continue |
解决冲突后继续 |
--abort |
中止 revert |
-m <编号> |
指定主线(用于 revert 合并提交) |
--edit |
编辑提交消息 |
示例
bash
# 撤销某次提交(生成一个新的反向提交)
$ git revert abc123
# 会打开编辑器确认撤销消息,然后创建新提交
# 撤销最近一次提交
$ git revert HEAD
# 撤销多个提交(按顺序,从旧到新)
$ git revert abc123 def456
# 撤销一个范围(不包括 abc123,包括 def456)
$ git revert abc123..def456
# 撤销但不自动提交(用于多次 revert 后统一提交)
$ git revert -n abc123 def456
$ git commit -m "撤销 abc123 和 def456"
# 撤销一个合并提交(需要指定主线,通常是 -m 1)
$ git revert -m 1 merge-commit-hash
# 处理冲突后继续
$ git revert abc123
# 解决冲突
$ git add .
$ git revert --continue
# 中止撤销
$ git revert --abort
reset vs revert 对比
| 特性 | reset | revert |
|---|---|---|
| 是否改变历史 | 是 | 否(追加新提交) |
| 适合公共分支 | 否(会破坏他人历史) | 是 |
| 撤销合并提交 | 可(但复杂) | 可(需 -m 参数) |
| 提交数量 | 减少 | 增加 |
| 可追溯性 | 丢失历史 | 完整记录 |
五、恢复丢失的提交/分支
10. git reflog -- 引用日志(救回误删的数据)
Git 会在本地记录 HEAD 和分支引用的每一次移动。即使执行了 reset --hard 或删除了分支,只要提交未被垃圾回收,就能通过 reflog 找回。
语法
bash
git reflog [<分支名>]
git reflog expire ... # 清理过期的引用(通常不需要手动)
示例
bash
# 查看 HEAD 的所有移动历史
$ git reflog
abc123 HEAD@{0}: commit: 添加新功能
def456 HEAD@{1}: commit: 修复bug
789xyz HEAD@{2}: reset: moving to HEAD~2
012abc HEAD@{3}: commit: 初始化项目
# 查看特定分支的 reflog
$ git reflog main
# 查看所有引用(包括远程跟踪分支、stash 等)
$ git reflog --all
# 找回被 reset --hard 丢弃的提交
$ git reflog
# 找到丢弃前的提交哈希(如 abc123)
$ git checkout abc123 # 进入分离 HEAD
# 或基于它创建新分支
$ git branch recovered-branch abc123
# 找回误删的分支(通过 reflog 找到分支最后一次指向的提交)
$ git reflog show feature/login
# 假设显示 abc123 HEAD@{5}: branch: Created from main
$ git checkout -b feature/login-recovered abc123
reflog 的默认过期时间
- 可访问的引用:90 天
- 不可访问的引用(如 reset 丢弃的):30 天
11. git fsck -- 文件系统检查,查找悬空对象
当 reflog 也被清理后,可以使用 git fsck 查找所有悬空对象(没有引用的提交)。
语法
bash
git fsck [选项]
常用选项
| 选项 | 说明 |
|---|---|
--unreachable |
显示所有不可达对象 |
--lost-found |
将悬空对象写入 .git/lost-found/ |
--no-dangling |
不显示悬空对象 |
--full |
完整检查 |
示例
bash
# 检查仓库完整性,显示悬空对象
$ git fsck
Checking object directories: 100% (256/256), done.
dangling commit abc123def...
dangling blob def456ghi...
# 只显示不可达的提交(可能是丢失的提交)
$ git fsck --unreachable --no-reflogs
# 将悬空对象保存到 lost-found 目录
$ git fsck --lost-found
# 查看悬空提交的内容
$ git show abc123
# 如果确认是需要恢复的提交,创建分支
$ git branch recovered abc123
12. git reflog + git cherry-pick -- 选择性恢复
如果只想恢复某个丢失的提交中的部分文件或更改,可以使用 cherry-pick。
bash
# 从 reflog 找到丢失的提交
$ git reflog
abc123 HEAD@{2}: commit: 重要功能
# 将该提交的更改应用到当前分支
$ git cherry-pick abc123
六、远程分支撤销与恢复
13. 撤销已推送的提交(安全方式)
方法1:使用 git revert(推荐)
bash
# 撤销最近一次推送的提交,并推送新提交
$ git revert HEAD
$ git push origin main
方法2:使用 git reset + --force-with-lease(危险,改变历史)
bash
# 本地回退
$ git reset --hard HEAD~1
# 强制推送(会覆盖远程历史)
$ git push --force-with-lease origin main
14. 恢复被删除的远程分支
如果远程分支被删除,但有本地跟踪引用或本地分支,可以重新推送。
bash
# 方法1:如果有本地分支
$ git push origin local-branch:remote-branch
# 方法2:如果有本地远程跟踪引用(origin/deleted-branch)
$ git push origin origin/deleted-branch:deleted-branch
# 方法3:从远程仓库的 reflog(如果托管平台提供)
# 联系管理员从服务器备份恢复
15. 撤销 git push --force 覆盖的远程分支
如果不小心用 --force 覆盖了远程分支,可以尝试从本地 reflog 找回。
bash
# 1. 在本地找到被覆盖前的提交(通过 reflog)
$ git reflog
abc123 HEAD@{5}: commit: 被覆盖前的最新提交
# 2. 强制推送回去
$ git push --force-with-lease origin abc123:main
16. git push --force-with-lease -- 安全的强制推送
bash
# 只有当远程分支与你本地记录的远程分支状态一致时才推送
$ git push --force-with-lease origin main
# 还可以指定预期的远程提交哈希
$ git push --force-with-lease=origin/main:abc123 origin main
七、文件级别的恢复
17. 从历史恢复某个文件的旧版本
方法1:git restore(推荐)
bash
# 从某个提交恢复文件到工作区
$ git restore --source=abc123 README.md
# 从上一个提交恢复
$ git restore --source=HEAD~1 src/app.js
方法2:git checkout(传统)
bash
# 从某个提交恢复文件
$ git checkout abc123 -- README.md
方法3:git show + 重定向
bash
# 查看旧版本内容并重定向到文件
$ git show abc123:README.md > README.md
18. 查看文件的修改历史并选择恢复
bash
# 查看文件的所有提交历史
$ git log --oneline README.md
# 查看某个历史版本的内容
$ git show abc123:README.md
# 对比两个版本
$ git diff abc123 def456 -- README.md
八、分支与标签的恢复
19. 恢复已删除的分支
bash
# 方法1:通过 reflog
$ git reflog
# 找到分支最后一次指向的提交(如 abc123)
$ git checkout -b recovered-branch abc123
# 方法2:通过 git fsck 查找悬空提交
$ git fsck --unreachable --no-reflogs | grep commit
$ git show abc123
$ git branch recovered abc123
20. 恢复已删除的标签
bash
# 如果标签在本地 reflog 中
$ git reflog show refs/tags/v1.0
# 从远程获取标签(如果已推送)
$ git fetch origin tag v1.0
# 从其他克隆获取
$ git fetch <other-remote> tag v1.0
九、暂存区与工作区的批量恢复
21. 放弃所有本地修改(回到上次提交的干净状态)
bash
# 丢弃所有已跟踪文件的修改
$ git reset --hard HEAD
# 删除所有未跟踪文件
$ git clean -fd
# 一步到位(组合命令)
$ git reset --hard HEAD && git clean -fd
# 仅丢弃已跟踪文件的修改(保留未跟踪文件)
$ git checkout -- .
22. 将当前工作区恢复到任意提交的状态(不改变提交历史)
bash
# 方法1:使用 reset --hard(会移动分支指针)
$ git reset --hard abc123
# 方法2:使用 checkout 进入分离 HEAD(不移动分支)
$ git checkout abc123
# 此时处于 detached HEAD 状态,可以查看/测试,但新提交不会属于任何分支
# 如果需要基于旧状态创建新分支
$ git switch -c new-branch abc123
十、交互式恢复与精细控制
23. git add -p 的反向操作 -- git reset -p
交互式地选择要从暂存区移除的修改块。
bash
# 交互式取消暂存
$ git reset -p HEAD
# 类似 add -p,选择要保留在暂存区还是移出
24. git checkout -p -- 交互式丢弃工作区修改
bash
# 交互式选择要丢弃的修改块
$ git checkout -p README.md
# 或使用 restore -p
$ git restore -p README.md
25. git stash 的撤销与恢复
bash
# 误删 stash(删除后但未执行其他操作)
$ git stash list
# 如果刚执行了 stash drop,可以通过 reflog 找回
$ git fsck --unreachable | grep commit
$ git show abc123
$ git stash apply abc123
# 清空所有 stash 后恢复(较困难,需从 reflog 找)
$ git reflog show refs/stash
十一、撤销合并操作
26. 撤销尚未提交的合并
bash
# 合并后出现冲突,想放弃合并
$ git merge --abort
# 如果合并已经完成但未提交,也可以
$ git reset --hard HEAD
27. 撤销已经提交的合并
方法1:使用 git revert(安全,推荐)
bash
# 撤销合并提交(需要指定主线)
$ git revert -m 1 merge-commit-hash
# -m 1 表示保留第一个父提交(通常是合并前的当前分支)
方法2:使用 git reset(改变历史,仅限私有分支)
bash
# 回到合并前的状态
$ git reset --hard HEAD~1
$ git push --force-with-lease
28. 撤销 git pull 的效果
bash
# 如果 pull 造成了合并冲突或不想保留合并
$ git reset --hard ORIG_HEAD
# 如果 pull 使用了 rebase
$ git reflog
# 找到 pull 前的 HEAD
$ git reset --hard HEAD@{1}
十二、恢复误修改的配置
29. 恢复 Git 配置
bash
# 查看全局配置
$ git config --global --list
# 删除某个配置项
$ git config --global --unset user.name
# 重新设置
$ git config --global user.name "正确的名字"
# 恢复默认配置(删除后重新添加)
$ git config --global --unset core.editor
十三、高级恢复技巧
30. 从 git gc 后的仓库恢复(数据恢复)
git gc 会打包和删除悬空对象。如果执行后丢失了数据,恢复难度极高,但仍有可能。
bash
# 1. 立即停止所有 Git 操作,防止进一步覆盖
# 2. 使用文件恢复工具(如 photorec)扫描 .git/objects 目录
# 3. 或者从备份/远程仓库恢复
31. 恢复被 git clean -fdx 删除的文件
git clean 删除的文件不经过暂存区,无法通过 Git 直接恢复。需要依赖:
- 文件系统的回收站
- 编辑器自动保存的备份
- IDE 本地历史
- 外部备份
32. 使用 git filter-branch / git rebase -i 撤销批量更改(重写历史)
bash
# 使用交互式变基删除或修改历史中的提交
$ git rebase -i HEAD~10
# 将不需要的提交改为 drop
# 或者使用 filter-branch 重写整个历史(谨慎使用)
$ git filter-branch --force --index-filter \
"git rm --cached --ignore-unmatch secrets.txt" \
--prune-empty --tag-name-filter cat -- --all
十四、撤销恢复命令速查表
| 场景 | 命令 |
|---|---|
| 丢弃工作区单个文件修改 | git restore <file> |
| 丢弃工作区所有修改 | git restore . |
| 取消暂存单个文件 | git restore --staged <file> |
| 取消所有暂存 | git restore --staged . |
| 删除未跟踪文件(预览) | git clean -n |
| 删除未跟踪文件 | git clean -fd |
| 删除包括忽略文件 | git clean -fdx |
| 修改上一次提交消息 | git commit --amend -m "新消息" |
| 添加上次遗漏的文件 | git add <file> + git commit --amend --no-edit |
| 撤销提交(保留修改在暂存区) | git reset --soft HEAD~1 |
| 撤销提交(保留修改在工作区) | git reset HEAD~1 |
| 彻底撤销提交(丢弃修改) | git reset --hard HEAD~1 |
| 撤销某次提交(生成新提交) | git revert <commit> |
| 撤销合并提交 | git revert -m 1 <merge-commit> |
| 查看操作历史 | git reflog |
| 找回丢失的提交 | git reflog + git checkout <hash> |
| 查找悬空对象 | git fsck --unreachable |
| 从历史恢复文件 | git restore --source=<commit> <file> |
| 放弃所有本地修改 | git reset --hard HEAD && git clean -fd |
| 中止正在进行的合并 | git merge --abort |
| 中止正在进行的变基 | git rebase --abort |
| 中止正在进行的 cherry-pick | git cherry-pick --abort |
| 撤销已推送的提交(安全) | git revert HEAD + git push |
| 撤销已推送的提交(强制) | git reset --hard HEAD~1 + git push --force-with-lease |
| 恢复误删的本地分支 | git reflog + git branch <name> <hash> |
| 恢复误删的标签 | git fetch origin tag <tagname> |
十五、常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
执行 reset --hard 后丢失了未提交的修改 |
工作区修改被覆盖 | 无法恢复(除非有编辑器备份) |
执行 reset --hard 后丢失了提交 |
HEAD 移动了 | 使用 git reflog 找回 |
| 误删分支 | git branch -D |
git reflog 找到分支最后一次指向的提交 |
| 误删 stash | git stash drop |
git fsck --unreachable 查找悬空提交 |
| 强制推送覆盖了远程分支 | git push --force |
从本地 reflog 恢复后重新推送 |
| 合并冲突想放弃合并 | 合并未完成 | git merge --abort |
| 变基冲突想放弃变基 | 变基未完成 | git rebase --abort |
| 误提交了敏感信息 | 文件被提交 | 使用 git filter-branch 或 BFG Repo-Cleaner 重写历史 |
git clean 删除了重要文件 |
未跟踪文件被删除 | 从系统回收站或备份恢复 |
执行 git gc 后丢失了对象 |
垃圾回收删除了悬空对象 | 很难恢复,依赖备份 |
十六、安全建议与最佳实践
-
养成使用
--dry-run的习惯在执行破坏性命令(如
reset --hard、clean、push --force)前,先用预览模式查看影响。 -
多用
revert代替reset在公共分支上,使用
revert而不是reset来撤销提交,避免影响协作者。 -
谨慎使用
--force使用
--force-with-lease代替--force,可以防止覆盖他人推送的提交。 -
定期备份
除了远程仓库,定期将
.git目录备份到安全位置。 -
善用
reflog
reflog是你的"后悔药",在遇到问题时首先查看它。 -
提交前检查
git status和git diff --cached检查即将提交的内容,避免错误提交。 -
使用
git stash临时保存在尝试危险操作前,先用
git stash保存当前状态。 -
配置自动推送保护
bash# 禁止强制推送到 main 分支(通过 Git 钩子或服务器端规则) -
学习
git reflog和git fsck这两条命令是数据恢复的核心,熟悉它们可以让你在多数情况下找回丢失的数据。
结语
Git 的撤销与恢复机制非常强大,只要理解了 Git 的数据模型(提交不可变、数据在删除后仍存在一段时间),你就能从容应对各种误操作。记住:
git restore:丢弃工作区/暂存区修改git reset:移动分支指针,可以撤销提交(改变历史)git revert:安全地撤销提交(追加新提交)git reflog:找回丢失的提交和分支git fsck:查找悬空对象
在实际工作中,遇到不确定的情况,先备份,再尝试。多练习这些命令,你会逐渐建立起对 Git 的信心,不再害怕犯错。