有一次,组里一个实习生态度特别好,git操作却把整个feature分支弄丢了:他以为先 git checkout dev 切走再回来就行,结果为了“清干净本地”,一条 git reset --hard origin/dev 把两天在本地写的东西全冲走了。当时整层楼都能听到他的哀嚎。
后来我帮他做了三件事:先让他别慌,再让他打开终端跑 git reflog,最后从一条悬空的提交记录里把分支完整捞了回来。整个过程不超过五分钟,代码一行没少。
这篇就是想把这种“急救能力”真正写透。既有新手装好Git之后第一步该配置什么、哪些命令会留下“后悔药”这样的保命常识,也有分支被删、提交被覆盖、push推错远端这类老手也会翻车的事故怎么低成本还原。你可以把它当成一张 Git 事故急诊表,收藏起来,下次遇到问题直接对号入座。
1. 先做心理建设:为什么你丢掉的提交,八成还能找回来
很多人一听说 commit 历史被覆盖、分支被删除,第一反应是“完了,代码没了”。实际上Git在设计上比大多数人想象中“抗造”。理解清楚它的存储模型,你就能在事故发生时不慌不忙地判断:这事能救,并且大概率能救回来。
1.1 Git 的底层是“追加日志”,不是“覆盖数据库”
你在一个仓库里看到的 master、feature/login 这些分支名,本质上只是一个个“指针文件”,指向某个 commit 的哈希。真正存代码的是 .git/objects 目录里的对象数据库,commit、tree、blob 都是新增文件,写进去之后基本不被修改。
你可以这样理解:分支名像书签,Git 数据库像图书馆。你把书签从某页撕掉,不代表那页书被烧了。只要没做深度清理,commit 对象仍然安安静静躺在数据库里,等待某个引用或者 fsck 工具重新发现它。
所以,本地提交被“丢掉”的绝大多数场景,恢复策略都是同一个套路:找到那个 commit 的真实哈希,然后把某个分支或者 HEAD 重新指过去。
1.2 误删现场,先别急着“补刀”
急救的第一原则是:在不确定恢复路径前,停止一切可能扩大伤害的操作。
下面这些命令在事故发生后要特别谨慎:
| 操作 | 危险程度 | 原因 |
|---|---|---|
git reset --hard <某个commit> |
高 | 会覆盖工作区和暂存区,未提交内容可能蒸发 |
git branch -D <分支名> |
中 | 删除分支引用,commit 需要靠哈希找回 |
git clean -fd |
极高 | 未跟踪文件不在 Git 对象库里,基本救不回来 |
git gc / git prune |
极高 | 会真正清理悬空对象,把“还能救”变成“没救了” |
我自己踩过最痛的一次是误删分支后手贱执行了 git gc,结果悬空对象被清掉,只能靠着另一个开发机器上的副本把代码拼回来。所以当你发现误操作,第一件事是打开一个新的终端窗口,先跑只读命令查看现场,不要继续在当前仓库里做任何写操作。
1.3 认清三个“区”,你就懂了一半撤销命令
要理解 Git 的误操作恢复,首先得说清楚 Git 日常操作的三个区域:
- 工作区:你正在编辑器里看到的文件。
- 暂存区(Index):通过
git add把文件的快照先放进去的地方。 - 版本库:
git commit后真正生成提交历史的地方。
很多撤销命令的本质,就是判断“我要把哪个区域的内容,覆盖到哪个区域”。
比如 git restore --staged <file> 是“把暂存区恢复成和版本库一致”,git restore <file> 是“把工作区恢复成和暂存区一致”。这个心智模型一旦建立,你看到命令就不会再靠死记硬背了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 新手高频翻车场景:add 错、commit 错、reset 过头怎么救
新手阶段最容易出事故的操作,其实就那么几个:把所有文件一股脑 git add .,提交完发现忘了加某个文件,或者刚提交就后悔想撤销。
2.1 不小心 add 了不该提交的文件
很多人的第一反应是用 git rm --cached 去删,这是把问题搞复杂了。正确做法分两步:
bash复制# 把文件从暂存区移出,但保留在工作区
git restore --staged <file>
# 老版本 Git(2.23 以前)用这条
git reset HEAD <file>
如果你已经提交了,并且之前忘了在 .gitignore 里排除掉某个本地文件,真正要处理的是让 Git 不再跟踪这个文件:
bash复制git rm --cached <file>
echo "<file>" >> .gitignore
git commit -m "chore: stop tracking file"
这里 --cached 的作用是只从版本库里移除,不会碰你本地磁盘上的文件。去掉跟踪关系之后再补 .gitignore,是避免同一个文件反反复复被误提交的根源解法。
2.2 刚 commit 完就想改:amend 的正确打开方式
最常见的情况是提交信息写错了,或者提交完发现漏了一个小文件。这时候如果提交还没 push 出去,git commit --amend 就是最优雅的后悔药:
bash复制# 修改上一次提交的说明文字
git commit --amend -m "feat: correct message"
# 想把漏掉的文件塞进上一次提交
git add forgotten.txt
git commit --amend --no-edit
注意 --no-edit 表示沿用上一次的提交信息,不用重新编辑器。当你在飞快地写代码时,这个参数能省下不少回车时间。
但要记住一个红线:只要这个提交已经 push 到公共分支、并且可能被其他人拉取过,就不要用 amend。因为 amend 本质是删除原提交、生成一个新提交,哈希已经变了。别人基于旧提交继续开发,你这边强行 amend 再强推,必然造成历史分叉。
2.3 提交完成后想撤销,soft/mixed/hard 怎么选
git reset 的参数是新手最容易混淆的地方。我给团队培训时常用一个类比:--soft 像只把日历翻回去,--mixed 是把台面上的东西收拾到抽屉里,--hard 是直接把屋子恢复到装修前的样子,桌子上没归档的文件也不见了。
| 参数 | HEAD 移动 | 暂存区 | 工作区 | 适用场景 |
|---|---|---|---|---|
--soft |
是 | 不动 | 不动 | 想重新提交,保留所有已暂存内容 |
--mixed(默认) |
是 | 重置 | 不动 | 撤销暂存,但保留本地修改 |
--hard |
是 | 重置 | 重置 | 彻底丢弃本地修改 |
git reset 不带参 |
否 | 重置 | 不动 | 取消已 add 的文件 |
给新手的建议是:把 reset --hard 当成高压电标签。90% 的“我想撤回刚才的提交”用 --soft 就够了。
bash复制# 撤销最近一次提交,但保留修改内容在暂存区
git reset --soft HEAD~1
如果连着 HEAD~1 范围都记不住,可以多用 HEAD^ 和 HEAD~ 的关系来辅助记忆:HEAD~1 表示上一个提交,HEAD~2 表示上两个提交。这只是个位移量,不是某种深奥语法。
2.4 提交之后才发现把分支切错了
还有一种极其常见的事故:你想在 feature/login 分支上开发,结果在 master 上写了一半,或者把两个分支的提交混在一起。
处理思路是:
- 先把所有手头工作提交到当前分支,避免丢失。
- 切到正确分支。
- 用
git cherry-pick <commit-hash>把刚才那个提交带到正确分支。 - 回到原分支,用
git reset --soft HEAD~1撤回那份“寄错地址”的提交。
只要 commit 是完整的、能定位到的,切错分支永远不是致命伤。真正致命的是在没 commit 的情况下直接切换分支,导致 Git 报错或者工作区内容互相覆盖。
3. 进阶救场核心:reflog 是怎么让已删分支和提交“复活”的
如果说第 2 章处理的是“后悔药”,这一章处理的是“事故现场重建”:分支被删、reset 之后找不到原 hash、stash 被误删。这些场景靠肉眼和常规 log 已经看不到了,此时必须依赖 Git 的自我体检机制。
3.1 reflog 的准确含义:它不是“命令历史”,是“引用移动记录”
很多人把 reflog 理解成命令历史,这是不对的。它也记录命令,但记录的本质是:每一次 HEAD 或分支引用的移动。
跑一下 git reflog 会看到类似输出:
text复制a1b2c3d HEAD@{0}: commit: feat: add login page
e4f5a6b HEAD@{1}: pull origin master
7a8b9c0 HEAD@{2}: checkout: moving from dev to master
HEAD@{0} 是当前 HEAD 位置,HEAD@{1} 是它上一次所在的位置,越往下越古老。你只要在 reset 之后立刻执行 git reflog,就能看到 reset 动作发生前 HEAD 指向哪个 commit,再把它指回去即可。
3.2 场景一:reset --hard 之后想把旧提交找回
比如你在 master 上做了三个提交,然后手滑跑了一条:
bash复制git reset --hard HEAD~3
git push --force
代码已 push 出去,本地又 reset 了,看起来死透了。实际上,只要 reset 前 HEAD 还在最新提交,reflog 里一定会留下记录:
bash复制git reflog
# 能看到类似:
# f3e2d1c HEAD@{0}: reset: moving to HEAD~3
# abc1234 HEAD@{1}: commit: feat: important work
找回的指令很简单:
bash复制git reset --hard abc1234
git push --force-with-lease
这里 --force-with-lease 比裸的 --force 安全很多,它只在你本地对远端状态的认知和实际远端一致时才允许强推,避免覆盖掉别人刚推送的新提交。我后面还会展开说。
3.3 场景二:分支被整体删除,reflog 里看不到分支名怎么办
分支删除后,如果你在 git reflog 里看到的只是 HEAD 相关记录,找不到原分支名,别急着放弃,还有两招:
第一招,看看本地远程跟踪分支是否还记录了旧状态。如果分支曾经 push 过,且远端还没有删除:
bash复制git fetch origin
git checkout -b feature/login origin/feature/login
如果远端分支也删了,那就需要靠 Git 的“孤儿对象体检”:
bash复制git fsck --lost-found
这个命令会扫描对象库里所有不被任何引用指向的对象,并在输出中列出 unreachable commit。如果运气好,你能看到类似:
text复制unreachable commit a9b8c7d
然后直接基于这个哈希把分支重新建出来:
bash复制git branch feature/login a9b8c7d
我在实际团队里帮人恢复过好几次分支,成功的命中率相当高。成功率高的前提同样是:删除分支后不要执行 git gc、git prune 这类严重清理命令。
3.4 场景三:stash 被 pop 出冲突后误 drop,怎么救
冲突解决得心烦意乱时,有些同学会直接执行 git stash drop 或者 git checkout . 把冲突现场清理掉。事后发现 stash 里的改动明明是需要的,怎么办?
思路同上:用 fsck 找 stash 对象。stash 本质也是 commit,只是没有分支指向它。找到悬空对象后,执行:
bash复制git stash apply <commit-hash>
可以把这个改动重新应用到当前工作区。注意是 apply,不是 pop,这样能避免再次丢失。
通常我建议所有刚接触 Git 的同事,在顺手执行危险命令之前都先备份一下当前 HEAD 哈希:
bash复制git branch backup-20240101
一行命令,几分钟后就能重建分支。这不是多余,事故发生的成本永远比备份成本高得多。
4. push 出去之后的事故:revert、force push与“队友已拉取”的选择题
很多人觉得本地恢复完就万事大吉,这是最大的误解。一旦 commit 已经 push 到远端,并且团队其他人很可能已经拉取,这时本地怎么改都只是掩耳盗铃,必须考虑“公共历史”的语义。
4.1 推错分支:想撤销远端提交的三种解法
先说一个最常见的事故:你本来想写 feature/login,代码已经 commit,结果 push 的是 master。
如果这个错误提交是 master 上最新的一条,且没有人基于它开发,推荐优先级从高到低分别是:
git revert <commit>:生成的是一条反向提交,不修改原历史,团队其他人 fetch 后不会有历史分叉。这种方式最安全,缺点是多出一条 revert 提交,不够“干净”。git reset --hard <原commit> && git push --force-with-lease:把本地指针拨回,再强推远端。好处是历史彻底干净,但前提是你能确认没有其他人已经拉取了这个错误提交。- 先
git revert止血,再另开清理任务:如果团队很大、协作频繁,先 revert 再说,干净历史以后再说。
记住一个原则:公共分支的安全优先级永远高于历史美观度。历史上多点一条 revert 提交,远没有覆盖队友代码的损失大。
4.2 revert、reset、force push 的对比,一张表说清楚
| 维度 | revert | reset | push --force-with-lease |
|---|---|---|---|
| 改变历史 | 不改变,新增反向提交 | 改变本地历史 | 改变远端历史 |
| 对他人影响 | 小,普通 pull 即可安全 | 大,可能导致提交分叉 | 极大,必须协调所有协作者 |
| 适合场景 | 已进入主干、多人协作的分支 | 只在本地、未与他人共享 | 自己独占的分支且确认远端没别人推过 |
| 可逆性 | 可再 revert 恢复 | 可从 reflog 恢复 | 能被旧 commit 再用 force 恢复,但复杂很多 |
4.3 为什么用 force-with-lease,而不是 force
裸 git push --force 是很多人的第一反应,但它像一个没有安全锁的保险柜。
想象一个场景:远端 master 原本在 commit A,同事小王在你上次 fetch 后又推了 commit B。你本地对远端的认知还停留在 A。这时你执行 git push --force origin master,想把自己的 commit C 强行覆盖上去,但实际上会把小王的 commit B 也从远端历史里抹掉。
--force-with-lease 会在强推前检查远端引用是否还是你“以为”的那个值:
bash复制git push --force-with-lease origin master
如果远端已经不是 A,命令会直接失败并提示你重新 fetch。这样既保护了自己,也保护了别人。
4.4 push 了包含密码的文件,命令救不了你
这是比误操作更严重的信息安全事故,必须单独拿出来说。
很多人以为“我 git rm 这个文件然后 commit、再 force push 一下,历史里就没有了”。这是完全错误的。只要你曾经把密钥 push 到远端,哪怕后来删除并强制重写,远端那套对象库里还可能残留原始 blob,而且任何 clone 过该仓库的人也已经在本地保存了那份文件。
正确操作步骤是:
- 立即吊销/轮换密钥,别指望 Git 操作能帮你“撤回”。
- 用
git filter-repo或 BFG Repo-Cleaner 等工具彻底清理历史中的敏感文件。 - 和团队成员确认,在约定时间统一进行硬重置,让所有本地副本重置到干净历史。
这条更像管理问题,但和 Git 紧密相关。遇到过一次你就懂,密钥泄漏的账,迟早要在走查会上还。
4.5 远端被强推覆盖,本地怎么找回旧提交
自己远端分支被别人的强推覆盖了,但旧提交还没全部丢失?如果在被覆盖前你本地 fetch 过那个分支,本地 refs/remotes/origin/<分支名> 的 reflog 里可能还留着旧状态:
bash复制git reflog show origin/feature/login
只要能看到旧哈希,就用它新建本地分支救回:
bash复制git branch feature/login-restored <old-hash>
如果是自己在服务器端操作的裸仓库,还可以登录到服务器上执行 git reflog 查看。不过多数托管平台的远端并不会保留 branch 的 reflog,所以这条经验更适用于自建 Git 服务器或者团队成员本地副本还留存的时候。
5. 分支合并翻车现场:merge 错分支、冲突误删的还原链路
merge 和 rebase 类操作里,有一类高频事故比“删分支”更容易让团队数据受伤:把 A 分支合并进了 B 分支、冲突解决时误删了代码、合并完成后发现整个方向就错了。
5.1 merge 错了分支,如何在不丢人的前提下安全还原
先说“正在 merge、还没提交”的现场:直接中止合并即可。
bash复制git merge --abort
这条命令会回到运行 git merge 之前的状态,工作区里已经产生的冲突标记也会一并清理。
如果是 merge 已经完成并且生成了一个 merge commit,想撤销这次合并,有三种路径:
- 如果这个 merge commit 是最新一条,直接用
git reset --hard HEAD~1(或回到 merge 前的 hash)。 - 用
git revert -m 1 <merge-commit-hash>生成一条反向合并。 - 借助
ORIG_HEAD:Git 在执行 merge、rebase、reset 等操作前会把原 HEAD 记录到ORIG_HEAD,短时间内可直接git reset --hard ORIG_HEAD回到操作前。
ORIG_HEAD 很方便,但有副作用:它是个“全局单一存储位置”,很快会被下一次危险操作覆盖。紧要关头最好还是打开 git reflog,找到最精确的那个目标点。
5.2 合并完成之后发现代码“少了”,先去两个 parent 里找
merge 提交和普通提交不同,它有两个 parent:一个是合并前当前分支所在的提交,另一个是被合入分支的提交。
如果合并后发现对方分支的某些改动没进到当前分支,或者在手动解决冲突时把整段代码删了,不要盲目回退整个 merge。更精准的做法是找到那个文件在“对方分支父母提交”里的状态,直接拉过来:
bash复制# 查看 merge commit 的两个父提交
git log --format="%H" -1 <merge-commit>
# 从第一个父提交恢复文件
git checkout <parent1-hash> -- path/to/file
# 从第二个父提交恢复文件
git checkout <parent2-hash> -- path/to/file
这一招在多人并行开发时特别管用。有时候你不需要撤销整个 merge,只是需要把某一块代码从另一半历史里“捞回来”。
5.3 再次踩坑预警:revert 了 merge 之后,重新 merge 会“失忆”
这个坑极其隐蔽,值得一字一字写清楚。
场景是这样的:你在一周前把 feature/payment 合并到了 master,后来发现 feature/payment 还没准备好,于是执行了:
bash复制git revert -m 1 <merge-commit-hash>
这条命令看起来让 master 回到了没合并 feature/payment 时的内容。问题来了:两周后 feature/payment 终于成熟,你想再次把它合并到 master,却发现 Git 提示“Already up to date”,或者即使合并了,代码改动也完全不进来。
原因在于 Git 合并是基于提交图的,它看到 feature/payment 分支的顶端提交已经存在于 master 的祖先历史中(你 revert 的只是这个 merge 带来的变更,没有删除 merge commit 本身),所以认为“这个分支已经合过”。
解决方案也很反直觉:你需要先 revert 掉那个 revert 提交,把当初被回滚的代码重新应用回来,然后再做一次 merge。
bash复制git revert <revert-commit-hash>
git merge feature/payment
如果不幸已经“空合并”了,处理起来会更麻烦,很可能需要手动 cherry-pick。所以对于大型 feature 分支,如果确定长时间不会再次合并,可以考虑先不要 merge、而是继续 rebase,或者明确标记状态,至少让团队知道这个分支是“已回滚但未撤销”。
5.4 ours/theirs 在 merge 和 rebase 里的语义反转
冲突解决时使用 git checkout --ours 还是 git checkout --theirs,无数人在这里栽过跟头。
规则是这样的:
- 执行
git merge other-branch时:ours 是当前分支,theirs 是正在被并入的分支。 - 执行
git rebase <base>时:ours 是 base 分支(也就是你 rebase 之后要待在上面的那个分支状态),theirs 是被重放的提交。
同样是 --theirs,在 merge 场景指的是对方分支的代码;在 rebase 场景却表示你要保留的、被 replay 过来的提交。很多人用错之后删错了代码,最稳妥的方式是不用 --ours/--theirs 做全局性决定,而是在具体文件上用 git show <commit>:<path> 先查看内容再决定。
真正动手做大规模代码取舍前,建议先执行:
bash复制git diff --name-only --diff-filter=U
看看到底有多少个冲突文件,避免“暴力解决”后造成隐蔽遗漏。
6. 配置层面的防线:装好 Git 之后,先做这些能救命的设置
事故往往发生在还没建立起正确工作习惯的阶段。很多问题不是你命令敲错,而是最开始环境就没配好:中文文件名乱码、每次 push 都要输密码、图形化工具背后偷偷跑的命令看不懂。这一章综合回答几个高频搜索词背后的真实需求。
6.1 安装之后第一件事:身份信息和换行符
新装 Git 后,很多人的第一反应是去搜“git 安装及配置教程”。其实最关键的配置只有那几个:
bash复制git config --global user.name "你的名字"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main
git config --global core.autocrlf input # macOS/Linux
git config --global core.autocrlf true # Windows
其中 core.autocrlf 解决的是换行符问题。Windows 用 CRLF,macOS/Linux 用 LF。如果团队里有人没配置过,你可能会看到明明只改了一行代码,整个文件 diff 全被标记为已修改的“幽灵换行”现象。
6.2 中文文件名乱码:core.quotepath 到底是干嘛的
这应该是所有中文 Git 用户都会遇到的问题:明明文件名是“需求文档.md”,git status 却显示成 "\351\234\200..." 一堆八进制转义字符。
这是因为 Git 默认对非 ASCII 字符做了转义,防止某些旧工具的兼容问题。要让它直接读取中文,执行:
bash复制git config --global core.quotepath false
这个配置对中文显示、git log 中对提交说明的中文内容也有帮助。虽然不影响文件实际内容,但它能显著降低你读输出时的“翻译成本”。
6.3 回答一个常见疑问:SourceTree 之类工具跑的命令为什么带一堆 -c 参数
很多从图形化工具转向命令行的用户都有同一个困惑:为什么工具自带终端里,git 命令总是带着一长串参数?比如:
bash复制git -c diff.mnemonicprefix=false -c core.quotepath=false --no-optional-locks status --porcelain -b
逐个拆开看:
-c diff.mnemonicprefix=false:-c表示临时设置配置项,不写入仓库配置文件;diff.mnemonicprefix控制 diff 中 a/ b/ 这类文件前缀。设成 false 时保留经典 a/ b/ 前缀,让外部 diff 工具不会因为local/remote这类前缀产生兼容性问题。-c core.quotepath=false:和上面一样,让工具显示中文路径时不转义。--no-optional-locks:这是一个容易被忽视的细节。Git 执行status等命令时本可以做一些 optional 的索引刷新,加上这个参数后,纯只读命令就不会尝试获取锁或写.git/index,避免与正在执行的其他 Git 进程相互阻塞。图形化工具加它,是为了防止用户在界面上点一下“刷新”,后台却和命令行操作抢锁。
如果你在终端里直接执行一条普通 git status,不会看到这些参数,但这不代表它们不重要。理解 -c 的语法结构,你就知道以后想“临时给某条命令加配置但不改仓库”时该怎么做。
6.4 免密配置,三种方式按场景选
“git 免密”是搜索热度一直很高的需求。这里给三种常见方案,以及它们的适用边界。
方案一:SSH 公钥方式(推荐,长期使用)
bash复制ssh-keygen -t ed25519 -C "your_email@example.com"
生成密钥后,把 ~/.ssh/id_ed25519.pub 里的内容添加到代码托管平台的 SSH keys 设置中,然后把仓库 remote URL 改成 SSH 格式:
bash复制git remote set-url origin git@github.com:你的账号/仓库.git
之后就无需再输密码。优点是密钥本身不出本地,安全性较高。注意私钥文件权限要收紧,否则系统不会认:
bash复制chmod 600 ~/.ssh/id_ed25519
方案二:HTTPS 凭据缓存(适合偶尔用一下)
bash复制git config --global credential.helper cache
git config --global credential.helper 'cache --timeout=3600'
作用是在内存中缓存密码或 token,默认 15 分钟,可以自己设置缓存时间。缺点是一段时间后仍要重新输入。
方案三:HTTPS 凭据 store(不推荐用于公用机器)
bash复制git config --global credential.helper store
这是最“方便”也最危险的方案:密码会明文存在 ~/.git-credentials 文件里。自己的电脑可以考虑,但公司共享开发机、云主机上绝对不要这么干。不少平台上个人访问令牌(token)的权限很大,泄露一次等于账号裸奔。
6.5 用 includeIf 实现对不同平台的账号隔离
进阶一点的用法:如果你同时维护公司仓库和个人项目,希望不同目录自动使用不同 Git 身份,可以在 ~/.gitconfig 里配置 includeIf:
ini复制[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-work
[includeIf "gitdir:~/personal/"]
path = ~/.gitconfig-personal
然后在 ~/.gitconfig-work 中配置公司使用的 user.name、user.email 或签名 key,在 ~/.gitconfig-personal 中配置个人身份。这样做以后,在对应目录下操作时不需要手工切换身份,也能避免因为忘记切账号而把个人邮箱提交进公司项目。
7. 两个一直被误以为“能毁灭仓库”的场景,其实没那么可怕
最后再聊两个容易让人吓得冒冷汗的经典场景,它们看起来像灭顶之灾,实际都能靠前面几章的知识处理掉。
7.1 场景一:误执行了 git clean -fd
git clean -fd 会删除所有未被 Git 跟踪的文件和目录。很多人以为执行了它就等于删除了本地所有代码,但严格来说,它只针对“未被 Git 跟踪”的文件。如果这些文件本来就存放在工作区里且从未被 commit 过,确实救不回来;但只要文件曾经提交过,哪怕后来被移动、改名,都可能从历史中找到内容。
正确的处理原则是:在执行 git clean -fd 之前,先执行 git clean -nd 看预览。-n 表示 dry-run,只列出会删除哪些文件,不会真的动手。我要求团队所有人在使用 clean 前必须跑一遍预览,这已经成为一条不成立的规定。
如果真误执行了且文件从未被 git 跟踪过,剩下的恢复手段只能依赖编辑器或文件系统的回收站机制,这不是 Git 能解决的范畴。所以这条更像是“事前防线”。
7.2 场景二:reset --hard 之后本地未提交的东西全没了
很多人 reset --hard 之后发现工作区里那些还没 add 的本地实验代码也没了,立刻五雷轰顶。但冷静下来想一下:reset --hard 把工作区重置成了某个 commit 能提供的状态,那些“不在任何 commit 里的内容”并没有被 Git 记录,自然也无法用 Git 指令恢复。
这里能给的经验是:如果你的工作区长时间堆着一堆未提交的试验代码,先用 git stash 或直接 commit 到临时分支。这个习惯可以在 30 秒内养成,却能在关键时刻避免整晚返工。
真正从事故事件中学到的事是:所有危险 Git 操作前,先确认当前工作区是干净的。可以用 git status --porcelain 快速判断,如果有输出,说明有未提交内容,先把它们存起来或提交掉。
个人项目里我特别依赖 git stash 和 git reflog 两个工具组合:一个是把“临时状态”安全保存,另一个是给“已经丢掉的提交”留后门。每次遇到同事求救,我都会先问三个问题:提交过没有?push过没有?有没有在误操作后跑过 gc?答案出来,基本就能判断这局的胜算。
希望这份从新手到高级的急救手册,能让你在下一次面对 Git 事故的时候,不再下意识刷新网页找教程,而是先打开终端,喝口水,然后从容地敲下 git reflog。代码不会说话,但它通常都还在老地方等你。
