Git 撤销与回滚:写错之后怎么救(9 种场景对照)

Git 的撤销让人慌,是因为命令看着都像能删代码。实际上只要先回答一个问题——代码现在在哪一层——选项立刻从十几个收敛到一个。

一、结论先给:对照表先存着

代码在哪 想达到什么效果 命令
工作区(没 add) 丢弃改动 git restore <file>
工作区(没 add) 先存起来 git stash -u
已 add(没 commit) 撤出暂存区,改动保留 git restore --staged <file>
已 commit(没 push) 撤销提交,改动回工作区 git reset --soft HEAD~1
已 commit(没 push) 撤销提交,连暂存一起退 git reset --mixed HEAD~1
已 commit(没 push) 连代码一起删(慎用) git reset --hard HEAD~1
已 push 保留历史、加一个反向提交 git revert <commit>
已 push 且必须抹掉 本地回退后强推 git reset --hard <commit> && git push -f
任何一步(包括删了的) 找回 git reflog

判断顺序只有一条:git status。 看懂它在说什么,就知道该用哪一行。

二、第一层:改了但还没 add

git status                      # 先看清楚有哪些改动
git diff <file>                 # 确认要扔的是什么
git restore <file>              # 丢弃单个文件(老版本用 git checkout -- <file>)
git restore .                   # 丢弃全部
git clean -fd                   # 删掉没被跟踪的新文件(先看 -n 预演)

顺序很重要:git clean -fdn 先预演,看清楚会删哪些,再去掉 n 真删。新建的文件(Untracked)restore 管不了,只有 clean 管。

不确定要不要删就先存着:

git stash -u          # -u 才会把新建文件一起收进去
git stash list
git stash pop         # 取回来
git stash apply       # 取回来但保留 stash 记录

stash 是个栈,跨分支也能用:在 A 分支 stash,切到 B 分支 pop,是搬代码的小技巧。

三、第二层:已经 add 了

git restore --staged <file>     # 从暂存区撤出,改动留在工作区
git restore --staged .          # 全部撤出

注意这一步不丢代码,只是把文件从"待提交"变回"已修改"。很多新手以为 git reset 才管用,其实 restore --staged 更精确、更安全。

想撤销 add 又顺便看看 staged 里到底有什么:

git diff --cached               # 暂存区与 HEAD 的差异

四、第三层:已经 commit,但还没 push

这是 reset 的主场,三种模式的区别一句话说清:

模式 HEAD 指针 暂存区 工作区代码 用途
--soft 退 保留 保留 只是想重新提交(改 message、合并提交)
--mixed(默认) 退 清空 保留 想重新挑文件再提交
--hard 退 清空 删除 彻底不要了
git reset --soft HEAD~1         # 最常用:提交记录没了,代码还在,可重新 commit
git reset --mixed HEAD~1        # 同上,但改动回到未 add 状态
git reset --hard HEAD~1         # 危险:那次提交的代码一起没了

HEAD~1 表示"上一个提交",HEAD~3 是往前三个,也可以直接写 commit hash。

最常见的安全用法是改提交信息或把两次提交并一次:

git reset --soft HEAD~2
git commit -m "合并后的提交信息"

五、第四层:已经 push 出去了

这里分两条路,选错会挨同事骂。

路径 A:git revert(推荐,协作分支必选)

git revert <commit>             # 生成一个"反向"的新提交
git revert HEAD                 # 撤销最近一次
git revert --no-commit HEAD~3..HEAD   # 撤销连续多个,合成一次提交
git push

revert 不动历史,只新增提交。已推送的公共分支(main / develop / release)一律用它。

路径 B:reset + 强推(只用于自己的分支)

git reset --hard <commit>
git push --force-with-lease     # 比 -f 安全

用 --force-with-lease 而不是 -f:前者会在"别人已经往这个分支推了新东西"时拒绝覆盖,后者会直接把同事的代码抹掉。这是唯一需要记住的区别。

强推前两件事:

  1. 确认分支是自己独占的(git log --oneline -5 看看有没有别人的提交)
  2. 记下当前 HEAD(git rev-parse HEAD),万一推错了还有得找

六、救命:reflog 找回"删掉"的提交

reset --hard 之后以为代码没了——只要没做 gc,提交对象还在,reflog 里记着每一次 HEAD 移动。

git reflog                       # 找那次操作的 hash
git reset --hard <hash>          # 回到那个点
git checkout -b rescue-branch <hash>   # 更稳:先开个分支保存下来

reflog 默认保留 90 天(未引用的对象 30 天 gc)。所以真删库之后立刻停手别提交,直接找 reflog。

找回单个文件更简单:

git log --oneline -- path/to/file     # 找文件最后一次出现的提交
git checkout <hash> -- path/to/file   # 把那个版本的文件取回工作区

七、进阶场景

只改上一次的提交信息(没 push)

git commit --amend -m "新信息"
git commit --amend --no-edit      # 只把当前改动并进去,不改信息

撤销某次 merge

git revert -m 1 <merge-commit>    # -m 1 表示以主分支那侧为准,数字写错会反着来

把某个提交挪到别的分支(cherry-pick)

git checkout target-branch
git cherry-pick <commit>
git cherry-pick --abort           # 冲突了不想处理

误删分支

git reflog --all | grep <branch-name>
git checkout -b <branch-name> <hash>

按内容找"那段代码去哪了"

git log -S "函数名或字符串" --oneline     # 搜索引入/删除该字符串的提交
git log --follow -p -- <file>            # 跟着文件改名看完整历史

八、几条纪律(比命令重要)

  1. 公共分支只用 revert,不用 reset --hard + 强推。
  2. 任何 --hard 之前先 git stash -u,10 秒的保险。
  3. 强推只用 --force-with-lease。
  4. 不确定就开分支试:git checkout -b test-reset,错了删掉,主干没动过。
  5. 提交前 git diff --cached 扫一遍,比事后救火便宜得多。

比对改动、看差异细节可以用 文本对比工具,把两段代码贴进去直接看哪几行变了;命令记不住时查 Git 命令速查。

九、小结

场景 一条命令
扔掉工作区改动 git restore .
撤出 add git restore --staged .
撤销 commit 但留代码 git reset --soft HEAD~1
撤销已 push 的提交 git revert <commit>
自己的分支强推 git push --force-with-lease
找回删掉的提交 git reflog → git reset --hard <hash>

Git 的设计是"几乎不真删东西",所以绝大多数误操作都能救。真正救不回来的是没提交过的工作区代码被 --hard 清掉——这也是为什么每次打算用 --hard,都该先 git stash -u。


相关工具:Git 命令速查 · 文本对比工具

延伸阅读:Cron 表达式写错就是生产事故 · Java 启动 jar 包:nohup、systemd、开机自启

还有 65 个免费在线工具

纯前端实现,不用注册,数据不上传服务器。

浏览全部工具