讲解
「撤销」在 Git 里有两个方向相反的词,分清它们能避开 90% 的自救事故。git reset 让分支指针「退回去」:git reset 目标提交 把当前分支移动到那个提交,之后的提交从分支上消失。它有三个档位,决定工作区和暂存区怎么处置:--soft 只动指针,改动全部留在暂存区(适合「这次提交太早了,改改再提」);--mixed(默认)动指针并把改动放回工作区(适合「这些改动要重新组织」);--hard 动指针并把工作区、暂存区一并清空(彻底丢弃,慎用)。
git revert 则是「用一个新提交抵消旧提交」:git revert 某提交 生成一个内容正好相反的新提交,历史只增不改。两者的根本区别决定了使用场景:reset 改写历史,只适合还没推送的提交;revert 不改写历史,是唯一可以安全用于已推送提交的撤销方式。在公共分支上要回退,永远用 revert——哪怕那个提交是你五分钟前推的,也可能已经被人拉走了。
还有一个组合场景值得记住:想完全丢弃本地最近几个还没推送的提交,git reset --hard HEAD~n 干净利落;reset 错了怎么办?提交其实没被删除,只是分支不指向它了,git reflog 记录了 HEAD 的每一次移动,找到旧哈希再 reset 回去即可(最佳实践一章有完整演示)。所以 Git 里的「删除」几乎都是可逆的,真正会丢的只有从未提交过的工作区改动。
示例
echo "版本2" >> app.js
git add app.js
git commit -qm "feat: v2"
echo "版本3" >> app.js
git add app.js
git commit -qm "feat: v3"
git log --oneline -3
git reset --soft HEAD~1
git status --short
git commit -qm "feat: v3(重新提交)"
echo "版本4" >> app.js
git add app.js
git commit -qm "feat: v4"
git reset HEAD~1
git status --short
git restore app.js
git revert --no-edit HEAD
git log --oneline -3
echo "要丢弃的实验" > junk.txt
git add junk.txt
git commit -qm "实验"
git reset --hard HEAD~1
git status --short
依次演示:--soft 撤销提交但保留暂存(改个信息重新提);默认 --mixed 撤销提交和暂存(改动回到工作区);revert 生成反向提交(历史前进着「撤销」);--hard 连工作区一起抹掉。
常见坑
- 在公共分支上 reset 已推送的提交:历史被改写,别人 pull 时历史分叉,团队灾难。公共分支只用 revert。
- --hard 之前不确认:它无差别丢弃所有未提交改动,包括你刚写了两小时的代码。执行前 git status 过一遍,或者先 stash 一份保险。
- 以为 reset 后提交真的没了:提交还在对象库里,git reflog 能找到,reset 错了可以再 reset 回来。大胆操作的前提是知道这条退路。
- revert 一个合并提交后困惑:revert 合并提交需要 -m 1 指定主线父提交;而且之后再想把那个分支合回来,Git 会认为「那些改动来过又被撤了」,需要特殊处理。合并提交的回退要格外小心。
小结
reset 移动分支指针改写历史(--soft/--mixed/--hard 决定改动去向),只用于未推送的提交;revert 生成反向提交安全撤销,公共分支的唯一选择。reflog 是 reset 的保险绳。下一章学习给重要节点打标签。