Git撤销和回退操作全流程
Git 的撤销回退要看阶段:未缓存用 checkout,已缓存用 reset HEAD,已提交用 reset --hard,已推送要么强推要么 revert。强推会丢历史要通知队友,revert 生成新版本更安全。
Git 里的撤销操作看起来很多,其实核心思路很简单:哪个阶段出错,就在那个阶段撤销回来。没理解清楚阶段的人,经常乱用 reset --hard 或者强推,结果队友的代码被冲掉。
先理解 Git 的三个状态
Working Directory(工作区)→ Staging Area(暂存区)→ Repository(仓库)。
每一步都可能出错,每一步也都有对应的撤销方式。
工作区修改 → git add → 暂存区 → git commit → 仓库
❌ ❌ ❌
checkout reset HEAD reset --hard
场景一:只在工作区改了,还没 add
用 checkout 扔掉本地改动。
# 单个文件
git checkout -- src/main.go
# 整个工作区
git checkout -- .
# 或者用新语法(Git 2.23+)
git restore src/main.go
执行后,工作区的改动全没了,回到最后一次 commit 的样子。
注意:这是不可逆的,改动真的丢了,所以动手前想清楚。
场景二:已经 add 到暂存区,还没 commit
用 reset HEAD 把文件从暂存区拉回工作区,改动还在。
# 单个文件
git reset HEAD src/main.go
# 全部文件
git reset HEAD
# 新语法
git restore --staged src/main.go
执行后,文件回到"已修改但未暂存"的状态,你可以继续改或者用上面的 checkout 扔掉。
场景三:commit 了,还没 push
这是最常见的场景,有几种处理方式。
只是想改最后一次 commit 的内容
比如漏了一个文件或者写错了 commit message。
# 补充文件(继续改工作区,然后 add,再执行下面的命令)
git add src/config.go
git commit --amend
# 这样会打开编辑器让你改 message
git commit --amend --no-edit # 不改 message,直接用上次的
--amend 是"修补"的意思,会用新的 commit 把上一次覆盖掉(Hash 值会变)。
想撤销这次 commit,改动还要保留
git reset --soft HEAD~1
~1 表示"往前 1 个 commit"。--soft 的意思是把 commit 撤销了,但改动还在暂存区里,你可以继续改或者重新 commit。
# 示例
git reset --soft HEAD~1
# 现在改动回到暂存区,你可以
git status # 查看现在的状态
git add <其他文件> # 继续 add
git commit -m "新的 message"
想撤销这次 commit,改动也要丢掉
git reset --hard HEAD~1
这次 commit 涉及的所有改动都回滚,工作区也干净了。一旦执行就回不来了(除非你能找到那个 commit 的 Hash,用 git reflog 看)。
想撤销多个 commit
git reset --hard HEAD~3 # 撤销最后 3 个 commit
想撤销某个中间的 commit,但保留之后的 commit
别用 reset,那样会把之后的都丢掉。应该用 revert 生成一个新 commit 来反向抵消。
git revert <commit-hash>
比如你第 5 个 commit 有 bug,第 6、7 个 commit 是基于第 5 个的。这时候用 revert 会生成新的第 8 个 commit,内容是把第 5 个的改动反向操作一遍。
# 查 commit 历史
git log --oneline | head -10
# 假设倒数第 4 个有问题
git revert <那个commit的hash>
# 会自动打开编辑器,让你改 revert 的 message,直接保存就行
之后的 commit 不动,历史也是完整的。
场景四:已经 push 了,想撤销
这是最容易踩坑的地方。
本地就你一个人,或者队友同意强推
用 reset --hard 撤销本地,然后强推。
git reset --hard HEAD~1
git push -f # 或 git push --force-with-lease
别用 -f,用 --force-with-lease,这样如果有人在你强推前又推了新内容,会自动拒绝,避免意外覆盖。
但即使用了 --force-with-lease,也要通知队友一下,说你要强推,免得他们的改动被冲掉。
有队友也在用这条分支
千万别强推。用 revert 生成新 commit。
git revert <commit-hash>
git push
这样历史保留、新同学能看到完整记录、没人的代码被冲。
# 示例
git log --oneline | head -5
# 8f3e2d1 (HEAD -> main) Merge pull request #123
# a1b2c3d Add feature X ← 这个有问题
# d4e5f6g Fix bug Y
# ...
git revert a1b2c3d
git push
# 现在历史变成
# 新的 commit Revert "Add feature X"
# a1b2c3d Add feature X(原样保留)
# d4e5f6g Fix bug Y
# ...
新 commit 的内容就是把 a1b2c3d 的改动反向操作,其他人拉下来会自动应用这个反向操作。
一个完整案例
假设你改了代码、add、commit、push,后来发现 push 出去的改动有 bug。
# 情况 1:只有你用这条分支
git reset --hard HEAD~1
git push --force-with-lease # 通知队友先
# 情况 2:有队友也在这条分支
git revert <那个commit的hash>
git push
# 情况 3:改动还没 push
git reset --soft HEAD~1 # 改动回暂存区
# 继续改...
git commit -m "fix: 修正之前的问题"
git push
撤销的底层逻辑
reset 和 revert 的根本差别:
- reset:直接把分支指针往回移(HEAD 指向更早的 commit),之后的 commit 记录就看不到了(但还在引用日志里,用 git reflog 能找回)。
- revert:创建新 commit,新 commit 的内容是反向操作,原来的 commit 都还在,历史完整。
所以 reset 是"改历史",revert 是"添加新内容来抵消"。
# 查看引用日志(即使 reset 了也能看到)
git reflog
# 会看到之前的 commit hash
# 02f3e2a (HEAD -> main) HEAD@{0}: reset: moving to HEAD~1
# a1b2c3d HEAD@{1}: commit: 有问题的改动
# 所以其实可以用 git reset --hard a1b2c3d 救回来
# 但一般不建议这样用,因为协作的时候会很混乱
小结
Git 撤销的核心就是看阶段选工具:
- 工作区改动 → checkout
- 暂存区 → reset HEAD
- 本地 commit → reset --soft/hard 或 revert
- 远程 commit → revert(有队友)或 reset + 强推(确定只有你)
最安全的原则是:改动已经共享了,就用 revert;改动只在本地,用 reset 无所谓。这样队友的历史永远不会被冲掉,大家各自安全。
