先问自己一句:推上去了吗
Git 撤销操作的所有选择,第一步都是同一个问题:这个提交推到远程了吗,别人拿到了吗。
没推送,历史是你一个人的,随便改写,改消息、删提交、回退到几天前,都无所谓,不会影响任何人。一旦推送出去,尤其是有同事在同一分支上干活,改写历史就等于把你的版本强行盖在别人头上,对方下次拉取时很可能直接冲突甚至丢代码。
所以下面分两块讲。先讲本地随便造的,再讲已经共享出去怎么收拾。
只改提交信息,没动代码
最常见的情况:提交信息打错字,或者写了一句毫无信息量的"改了一下"。
如果这是最新一次提交,一条命令就够。执行后会打开编辑器让你改消息,保存退出即可。
# 修改最新一次提交的信息
git commit --amend
# 想顺手把刚忘了的文件也加进去
git add 忘了的文件.txt
git commit --amend --no-edit
要注意 --amend 改的不只是消息,它会把当前暂存区的内容一并并进这个提交。如果你在提交后又改了点东西但还没 commit,那些改动会被悄悄带进去。想保持纯粹只改字,确保暂存区是干净的。
另外,--amend 生成的是一个全新的提交对象,哈希值会变。这就是为什么推送过的提交用 amend 之后再推会报冲突。
多提了文件,想从提交里拿掉
提交完发现把一个日志文件、配置文件或者大压缩包一起提交了。
# 把这个文件从暂存区撤出(保留本地改动)
git restore --staged 出错的文件.log
# 然后再 amend,重新生成一个不含它的提交
git commit --amend --no-edit
如果这个文件本来就该被忽略,记得补一条 .gitignore,不然下次还会手滑。已经推送过的,看后面"已经共享出去"那一节。
撤回提交:reset 的三种模式
git reset 是把当前分支的指针往回挪。它有三个常用模式,区别在于挪完之后你的工作区是什么状态。
| 模式 | 提交历史 | 文件改动 | 适用场景 |
|---|---|---|---|
| --soft | 回退 | 保留在暂存区 | 想重新组织这个提交的内容 |
| --mixed(默认) | 回退 | 保留在工作区,未暂存 | 想重新挑选要提交哪些文件 |
| --hard | 回退 | 直接丢弃 | 真不想要这些改动了 |
# 撤销最近一次提交,改动留在暂存区
git reset --soft HEAD~1
# 撤销最近一次提交,改动留在工作区
git reset HEAD~1
# 撤销最近一次提交,改动一起丢掉(危险)
git reset --hard HEAD~1
# 回退到指定提交
git reset --hard a1b2c3d
--hard 是真的丢东西,动手前确认一遍。不过也别太慌,后面会讲怎么做补救。
已经推送出去了怎么办
这是最容易被搞砸的地方。
如果分支只有你一个人用,可以 amend 或 reset 之后强制推送。命令是 git push --force-with-lease,注意用这个而不是 --force:它会先检查远程有没有你不知道的新提交,有的话拒绝推送,避免把别人的工作覆盖掉。
但分支有别人在用,就别碰历史。改用 revert:它不是删掉旧提交,而是新建一个内容相反的提交来抵消它。
# 建一个反向提交,抵消指定提交的改动
git revert a1b2c3d
# 撤销最近一次提交
git revert HEAD
# 撤销一个合并提交,需要指定保留哪条主线
git revert -m 1 合并提交的哈希
revert 的好处是历史完整、不影响任何人,代价是日志里会留下"提交 + 撤销"两条记录。多人协作的项目通常更愿意接受这点冗余,而不是承担改写历史的风险。
已经丢了的东西怎么找回来
Git 其实很少真的删数据。它管的是"有没有东西指向这个提交",只要还有引用,对象就在。
git reflog 记录的是 HEAD 的移动轨迹,你做的每一次 commit、reset、checkout 都在里面。
# 查看 HEAD 的移动历史
git reflog
# 输出大概长这样:
# a1b2c3d HEAD@{0}: reset: moving to HEAD~1
# f4e5d6c HEAD@{1}: commit: 加了个功能 <-- 就是它
# 回到 reset 之前那个状态
git reset --hard f4e5d6c
两个前提得说清楚:一是被 reset 掉、但从来没提交过的改动,reflog 也救不了,因为 Git 根本不知道它存在过;二是这个记录不会永远保留,默认 30 天,而且手动执行过 git gc 之后可能提前消失。发现丢了东西,越早动手越好。
误提交了密钥或大文件
这一类要单独拎出来说,因为它的处理方式和前面都不同。
把密码、Token、私钥提交进仓库,然后把文件删掉再提交一次,看起来像是解决了,其实没有。Git 保留了历史版本,任何人 clone 仓库、翻一下旧提交,照样能把密钥翻出来。私有仓库也不保险,谁能访问仓库谁就能看到。
正确的处理顺序是先把密钥作废,再去清理历史。顺序不能反,先清历史再换密钥,中间那段时间它仍然是有效的。
清理历史需要重写所有涉及的提交,普通命令做不到,得用专门的工具,比如 git filter-repo,或者早期的 BFG Repo-Cleaner。这类操作会改变所有提交的哈希值,等于重建了整个历史,因此所有协作者都必须重新克隆仓库,不能只是拉取。执行前一定先做好完整备份。
顺带说一句,大文件塞进仓库也是类似的麻烦。它会让仓库体积永久性变大,因为对象都在历史里躺着。清理方式和密钥一样,都需要重写历史。与其事后补救,不如提前用 .gitignore 拦住。
一张对照表收尾
| 你想做的事 | 用哪个 | 能改历史吗 |
|---|---|---|
| 改最新提交的消息 | git commit --amend | 能(本地用) |
| 从提交里拿掉某个文件 | git restore --staged + amend | 能(本地用) |
| 撤销本地提交,保留改动 | git reset --soft / --mixed | 能(本地用) |
| 撤销已推送的提交 | git revert | 不能(安全) |
| 找回被 reset 的提交 | git reflog + reset | 看情况 |
| 清除历史里的密钥 | git filter-repo | 能(需全体重克隆) |
记住开头的那个判断就够了:没推送的,随便改;推送过的,优先用 revert 加新提交,别去动历史。
常见问题 FAQ
已经 push 到远程的提交,还能改提交信息吗?
技术上可以,用 git commit --amend 改完再 git push --force。但如果这个分支有别人在用,强制推送会覆盖他们的工作,风险很大。安全的做法是先跟同事打招呼,或者改用 git revert 建一个新提交来抵消,虽然历史里会留下两条记录,但不会影响别人。
git reset --hard 之后代码还能找回来吗?
多数情况能。Git 会在本地保留一段时间的对象记录,用 git reflog 可以看到 HEAD 的移动历史,找到 reset 之前那个提交的哈希,再 git reset --hard 回去即可。但这个恢复有前提:操作后没有执行过垃圾回收(gc),且时间不能太久,通常默认保留 30 天。被 reset 掉而且从没提交过的改动则找不回来。
reset、revert、checkout 这三个到底有什么区别?
reset 是把分支指针往回挪,历史被改写,适合本地未推送的提交。revert 是新建一个反向提交来抵消某次改动,历史保留完整,适合已经推送或已共享的提交。checkout(新版 git 里推荐用 restore 和 switch)主要用于切换分支或恢复文件内容,它不改变提交历史。
误提交了大文件或密码,删掉文件再提交就没问题了吗?
不够。Git 会保留文件的历史版本,即使当前提交里删掉了,旧提交里依然能翻出来,别人 clone 仓库照样拿得到。正确做法是立刻把密钥作废并更换,然后用 git filter-repo 或 BFG 这类工具重写历史,把那部分记录从所有提交里彻底清除,最后强制推送并通知所有协作者重新克隆。只删文件不换密钥,等于密码还挂在外面。
