Git 的可怕之处在于“撤销”有太多种做法,选错了会把改动弄丢。先记一张决策表。

撤销的四种粒度

想撤销什么 命令 影响范围
工作区某个文件的改动 git restore <file> 只丢未暂存的修改
已 git add 但未提交 git restore --staged <file> 只移出暂存区,改动还在
最近一次提交(未推送) git reset --soft HEAD~1 提交没了,改动回到暂存区
已推送的提交 git revert <commit> 新增一个反向提交,历史不改写

核心原则:已推送的历史不要改写。 reset 之后 push 需要 --force,会把别人的提交一起冲掉;revert 才是协作环境下的正确做法。

reset 的三种模式

git reset --soft  <commit>   # 只动 HEAD:改动留在暂存区
git reset --mixed <commit>   # 默认:改动留在工作区(未暂存)
git reset --hard  <commit>   # ⚠ 改动全丢,工作区被强制覆盖

--hard 会丢弃已跟踪文件的本地改动(未跟踪且未被忽略的文件保留)。所以:

  • 部署机上用它前先确认“这里不该有本地改动”;
  • 它不会删除被 .gitignore 忽略的文件(比如 .env、logs/)。

误提交了敏感信息或大文件

# 刚从暂存区加进去、还没提交
git restore --staged .env && echo '.env' >> .gitignore

# 已经提交但还没推送:改写那一次提交
git rm --cached .env && git commit --amend

# 已经推送:从**最新提交**里移除
git rm --cached .env && git commit -m 'chore: 移除误提交的 .env'

⚠ 重要认知:git rm --cached 只能让文件从当前版本消失,历史提交里仍然能检索到。要彻底清除必须重写历史(git filter-repo / BFG)+ 强制推送,并让所有人重新克隆——代价很大。所以密钥一旦提交过,就应该直接轮换,而不是指望删干净。

reflog:误操作的最后一根救命绳

git reflog                     # 列出 HEAD 的移动历史(含被 reset 丢掉的提交)
git reset --hard HEAD@{2}      # 回到当时的状态
git checkout -b rescue <sha>   # 或先建分支保住它

reflog 默认保留 90 天。在慌乱中执行 --hard 之前,先记住这个命令还能救回来。

与项目流程相关的小事

  • 提交前用 git status --short 与 git diff --cached 双重确认(别用 git add -A 一把梭,会把临时文件一起提交)。
  • 提交信息写“为什么”而不是“改了什么”——改了什么看 diff 就知道。
  • 打回退点用 git tag -a v1.0.0 -m '...',比记住一串 sha 可靠。