01.为什么代码需要版本控制:从手工备份到分布式 Git.

2 阅读5分钟

本地写错、加错、提交错以后怎么办

“怎样撤销 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 的记录中,不能假设它一定可以恢复。

已经共享的提交为什么要另作处理

已经推送并被其他人依赖的历史,不适合随意用 resetamend 改写。团队通常会创建一个新的反向提交,让错误的效果被撤销,同时保留发生过什么。

这属于共享历史的安全撤销,后面会在“如何安全撤销已经共享的提交”中单独展开。

选择方法时只问两件事

错误位置想保留文件修改吗处理方向
已跟踪文件的工作区恢复工作区文件
暂存区取消暂存
最近本地提交amend 或 reset 后重新组织
已共享提交通常保留历史新建反向提交

先判断位置,再判断内容是否保留,比背一长串“撤销命令”可靠得多。

总结

恢复操作真正选择的不是命令,而是两件事:历史要不要改写,文件内容要保留在哪一层。只要先把这两个问题说清楚,就不容易把“取消暂存”做成“删除修改”。

参考资料