本地写错、加错、提交错以后怎么办
“怎样撤销 Git 操作”不是一个足够具体的问题。同样叫“写错了”,内容可能还在工作区,可能已经进入暂存区,也可能已经成为提交。
恢复之前最重要的动作不是复制一条命令,而是确认错误位于哪一层,以及修改内容是否还要保留。本文只处理尚未推送、没有被别人依赖的本地修改和提交;已经共享的历史不要直接照搬本文中的改写操作。
先准备一个独立练习仓库
不要在真实项目中试验恢复命令。下面每种情况都从这个只有一个初始提交的干净仓库开始;要比较不同方案时,重新创建练习目录,不要把几个场景连续执行。
mkdir recovery-lab
cd recovery-lab
git init
git config user.name "Git Blog Lab"
git config user.email "git-blog@example.invalid"
printf "base line\n" > note.txt
git add note.txt
git commit -m "docs: add initial note"
先观察,不要先破坏
git status --short
git diff
git diff --cached
git log --oneline -5
这组只读检查分别帮助判断工作区、暂存区和提交历史。位置不同,恢复方法也不同。
情况一:工作区内容写错了
下面的例子只针对已经被 Git 跟踪、尚未重新暂存的修改。先制造并检查错误:
printf "wrong line\n" >> note.txt
git diff -- note.txt
风险: 对整个路径执行
git restore会用暂存区中的版本覆盖该路径尚未暂存的修改,这些内容没有进入 Git 历史,Git 通常无法替你找回。文件中如果混有需要保留的修改,应先手工编辑,或者在熟悉交互操作后使用git restore --patch。
确定整份文件的未暂存修改都不再需要以后:
git restore note.txt
默认情况下,文件会恢复为暂存区中的内容。
普通未跟踪文件不在暂存区中,不能用这个命令恢复到某个旧版本。
情况二:文件不该进入暂存区
先制造一份想保留、但暂时不提交的修改:
printf "keep this line\n" >> note.txt
git add note.txt
git diff --cached -- note.txt
git restore --staged note.txt
没有显式指定来源时,这个操作让暂存区中的 note.txt 回到 HEAD 中的版本,同时保留工作区文件内容。它表达的是“这次先不提交”,不是“把修改删掉”。
执行以后应该再次查看:
git status --short
git diff
短状态应显示为 M note.txt:左列为空,说明没有暂存变化;右列是 M,说明工作区修改还在。
情况三:最近一次提交说明写错了
先创建一笔提交说明写错的本地提交:
printf "recovery note\n" >> note.txt
git add note.txt
git commit -m "doc: wrong message"
历史改写提示: 只有确认这笔提交尚未推送、也没有被别人依赖时,才执行
amend。
git commit --amend -m "docs: explain local recovery"
amend 会创建一个新提交替换当前分支顶端的提交。提交的父提交和作者默认保持不变,但提交者信息、提交说明或快照发生变化时,提交标识通常也会变化。
情况四:最近一次提交漏了文件
假设本地功能提交已经完成,随后才发现漏掉测试文件:
printf "feature line\n" >> note.txt
git add note.txt
git commit -m "feat: add recovery example"
printf "test case\n" > test.txt
git add test.txt
历史改写提示: 以下操作同样会替换当前提交,只适合尚未共享的历史。
git commit --amend --no-edit
--no-edit 保留原提交说明,新提交则包含当前暂存区中的完整内容。
情况五:想拆掉最近一次本地提交
先创建一笔准备重新组织的本地提交:
printf "change to reorganize\n" >> note.txt
git add note.txt
git commit -m "feat: temporary combined change"
这里的 HEAD~1 表示当前提交的第一个父提交。
危险操作提示: 以下三种方式都会移动当前分支。
--hard还会让暂存区和工作区匹配目标提交,覆盖受跟踪文件的修改,并可能覆盖妨碍写入目标版本的未跟踪路径。执行前必须确认git status和差异;要比较三种模式,应分别重新创建练习仓库。
三种 reset 的差别,可以从“提交移动以后,内容保留在哪一层”理解:
| 方式 | 提交位置 | 暂存区 | 工作区 |
|---|---|---|---|
git reset --soft HEAD~1 | 后退 | 保留 | 保留 |
git reset --mixed HEAD~1 | 后退 | 重置 | 保留 |
git reset --hard HEAD~1 | 后退 | 重置 | 重置 |
--soft 适合重新组织刚完成的本地提交;默认的 --mixed 会取消暂存但留下文件变化。
被移出当前分支历史、但已经形成过的提交,还有专门的找回方法,后续专题再处理。这里需要先记住:从未提交而被 --hard 覆盖的工作区内容通常不在 Git 的记录中,不能假设它一定可以恢复。
已经共享的提交为什么要另作处理
已经推送并被其他人依赖的历史,不适合随意用 reset 或 amend 改写。团队通常会创建一个新的反向提交,让错误的效果被撤销,同时保留发生过什么。
这属于共享历史的安全撤销,后面会在“如何安全撤销已经共享的提交”中单独展开。
选择方法时只问两件事
| 错误位置 | 想保留文件修改吗 | 处理方向 |
|---|---|---|
| 已跟踪文件的工作区 | 否 | 恢复工作区文件 |
| 暂存区 | 是 | 取消暂存 |
| 最近本地提交 | 是 | amend 或 reset 后重新组织 |
| 已共享提交 | 通常保留历史 | 新建反向提交 |
先判断位置,再判断内容是否保留,比背一长串“撤销命令”可靠得多。
总结
恢复操作真正选择的不是命令,而是两件事:历史要不要改写,文件内容要保留在哪一层。只要先把这两个问题说清楚,就不容易把“取消暂存”做成“删除修改”。