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:前者会在"别人已经往这个分支推了新东西"时拒绝覆盖,后者会直接把同事的代码抹掉。这是唯一需要记住的区别。
强推前两件事:
- 确认分支是自己独占的(
git log --oneline -5看看有没有别人的提交) - 记下当前 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> # 跟着文件改名看完整历史
八、几条纪律(比命令重要)
- 公共分支只用
revert,不用reset --hard+ 强推。 - 任何
--hard之前先git stash -u,10 秒的保险。 - 强推只用
--force-with-lease。 - 不确定就开分支试:
git checkout -b test-reset,错了删掉,主干没动过。 - 提交前
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。