
1. 先搞清楚退的三块地盘命令才不会乱敲git 的版本回退、文件恢复、恢复误提交这几件事看上去是一类问题实际上对应着完全不同的三套机制。我在带新人的时候发现绝大多数事故都不是命令记不住而是没先判断我要退的东西现在待在哪一层抓到 reset 就往上敲结果一敲下去工作区干净了半天的改动也跟着干净了。所以在动手之前必须先回答两个问题这份代码有没有被我 commit 过如果 commit 过有没有被 push 到远端这两个问题的答案直接决定你能用哪条命令、以及用了之后会不会影响到别人。下面把这三层地盘拆开讲清楚后面的所有命令你都能自己对上号。1.1 工作区、暂存区、本地仓库同一份代码的三个副本git 把一份代码在不同阶段存放在三个位置。工作区就是你在编辑器里打开、正在改的那个目录你按下保存键改动只落到磁盘上git 完全不知道。暂存区也叫索引index是git add之后内容停留的地方它相当于你在超市结账前把商品放进购物车还没付款。本地仓库是git commit之后内容进入的地方每一次提交都是一份带哈希值的完整快照从此它就成了历史的一部分除非你主动去动历史否则它一直待在那里。搞清这三层之后回退这个词就有了明确含义往工作区退还是往暂存区退还是把仓库的 HEAD 指针往回挪。这三个动作对应的命令完全不一样危险程度也差着量级。往工作区退最坏结果是你重新改一遍把 HEAD 往回挪并且加了--hard没备份的改动就直接从磁盘上消失了连回收站都没有。还有一个容易忽略的第四层——远端仓库。它的特殊之处在于一旦被推上去历史就不再只属于你一个人别人可能已经基于那条提交拉了代码、建了分支。这时候你对历史的操作成本就不再只是我自己的事了。1.2 一张判断表先定位再选命令下面这张表是我自己在用的速查逻辑遇到回退需求先对着它走一遍基本能避开八成的误操作。当前状态典型场景推荐命令风险等级改了没 add代码改废了想还原git restore file低改动本来就没保存已 add 没 commit加错文件、想撤出暂存区git restore --staged file低文件还在已 commit 没 push提交信息写错、漏了文件git commit --amend中改写本地历史已 commit 没 push想整体撤销最近一次提交整个不要了git reset --soft HEAD~1中注意别加--hard已 push 到共享分支部署出问题要整体撤回git revert sha低对他人影响最小已 push只有自己用个人分支要干干净净git reset --hard 强制推送高务必先备份已经乱成一团reset 敲错、分支被删git reflog中属于最后的保险注意表里风险等级指的是操作本身对已有数据的破坏可能性不是指命令难度。revert看起来复杂但它是唯一不会丢东西的做法。1.3 reset 的三种模式差的就是动谁不动谁git reset是回退里最容易出事的一个命令因为它有三个模式默认模式还未必是你想要的那个。它的本质是把当前分支的 HEAD 指针挪到某个提交上而三个模式的区别在于它顺手重置了哪些区域。模式HEAD 指针暂存区工作区一句话理解--soft移动不动不动只退提交记录改动全留在暂存区--mixed默认移动重置不动退提交并把改动退回到工作区--hard移动重置重置三处一起退没提交的东西全没我个人的习惯是永远不写裸的git reset三个模式必须显式选一个。因为默认的--mixed在很多场景下并不是你要的——比如你想把最近一次提交的内容整体拆成两个提交那要的是--soft而如果你手一抖敲了--hard还没加目标提交那就会把当前工作区的所有未提交改动一起清空。还有一个特别值得记住的点git reset在移动 HEAD 之前会把原来的 HEAD 位置记录到ORIG_HEAD里。这就是为什么很多事故还能救回来——git reset --hard ORIG_HEAD就能原地返回。这个细节后面讲 reflog 时还会用到。2. 还没提交就出事删掉的文件和改废的代码怎么捞未提交层面的恢复是整个回退体系里最轻松的部分因为东西都还在。但这里有个关键前提文件必须被 git 跟踪过。如果一个文件从来没被git add过那它在 git 眼里根本不存在任何 git 命令都救不了它。这条界线要刻在脑子里后面第 2.4 节会专门讲这条线划在哪里。2.1 git status 是排查的第一步别急着敲命令我见过太多次这样的场面有人发现文件没了第一反应是打开搜索引擎然后凭记忆敲一条命令结果把本来还能救的状态改得更糟。正确的第一反应只有一条命令git status。它的输出会明确告诉你文件处在哪个状态。Changes not staged for commit说明改动在工作区没进暂存区Changes to be committed说明已经add了Untracked files说明这些文件从没被跟踪过是恢复的盲区deleted:说明文件被删了但这个删除动作还没提交。每一种状态的恢复手法都不一样先看清楚再动手。顺便说一句如果git status输出的中文路径变成了一串\344\275\240\345\245\275这样的八进制转义别慌这只是 git 默认把非 ASCII 字符转义显示了。加参数git -c core.quotepathfalse status就能正常显示中文。很多 IDE 调用 git 时自动带的就是这组参数。2.2 只改了没暂存、暂存了没提交restore 的两个方向git restore是 Git 2.23 之后引入的命令专门用来替代过去那个职责混乱的git checkout。它的方向感非常清楚默认从暂存区恢复到工作区加上--staged就从 HEAD 恢复到暂存区。# 改动只在自己手上想丢掉工作区的修改回到上次 add 之后的样子 git restore src/main.js # 已经 add 了想把它从暂存区拿出来但保留工作区改动 git restore --staged src/main.js # 两个都要既撤出暂存区也丢掉工作区改动 git restore --staged --worktree src/main.js如果你维护的是老版本环境对应的老写法是git checkout -- src/main.js丢弃工作区改动和git reset HEAD src/main.js撤出暂存区。功能等价但checkout一个命令兼管切换分支、恢复文件、创建分支三件事用的时候容易误伤这也是官方后来拆出restore和switch的原因。这里有个新手常见的困惑我明明只想撤一个文件为什么git restore --staged之后文件内容看起来没变因为它本来就不该变。--staged只动暂存区工作区那份改动原封不动你的编辑器里看到的当然还是改过的版本。要连工作区一起回退得再加--worktree。2.3 文件已经被 rm 掉但没提交从版本库里直接取回这是最典型的文件恢复场景手一抖rm -rf了某个目录或者编辑器里误删了文件但这个删除动作还没 commit。因为文件的内容在最后一次提交里存得好好的所以恢复起来非常干净。# 最省事的做法让 git 把工作区恢复到 HEAD 的状态 git restore src/utils/ # 老写法效果一样 git checkout -- src/utils/如果这个删除动作已经提交了情况就换了一档。你不能再用restore从 HEAD 取因为 HEAD 里已经没有这个文件了。这时候要指定一个还包含该文件的提交作为来源# 从上一个提交里把文件取回来同时放进暂存区 git checkout HEAD~1 -- src/utils/deleted.js # 新写法注意 --source 的用法 git restore --sourceHEAD~1 --worktree --staged src/utils/deleted.js我更喜欢git checkout commit -- path的写法因为它取出来的文件会自动进入暂存区你可以直接git status看一遍确认内容对了再提交。而git restore --source默认只改工作区如果你不习惯这个差异很容易以为自己取错了。至于这个删除动作已经被推送到远端、别人也拉过了——那就别想着删历史了直接补一次恢复提交更省事把文件取回来git commit -m 恢复误删的 xxx 配置推上去就完事了。历史里留一条删了又加回来的记录在团队协作里其实比强行改写历史更受欢迎。2.4 git clean 误删的未跟踪文件git 真的救不了前面反复强调过一条界线这里必须单独拎出来说git 只对进入过版本库的内容负责。用git clean -fd清掉的未跟踪文件、新建了还没add的目录git 里没有任何记录reflog也没有、fsck也找不到因为它压根就没进过对象数据库。这也是git clean比git reset --hard更危险的地方——后者至少还有 reflog 兜底前者是真的没有后悔药。所以我给自己定了一条铁律git clean必须先用-n或--dry-run预览一遍。# 先看看会删掉哪些东西这一步千万别省 git clean -nd # 确认没问题再真的删-f 是必须的保险开关 git clean -fd # -x 会连 .gitignore 忽略的文件一起删风险最高慎用 git clean -ndx被git clean删掉的文件只能从文件系统层面去救比如文件系统的快照、备份、或者系统自带的回收机制。这里要补充一个很多人不知道的物理限制如果磁盘是 SSD 且开启了 TRIM文件被删除后底层存储块会被很快回收恢复概率会大幅下降。所以出事后第一件事是停止往这块盘上写入新数据包括别往同盘下载东西、别编译生成一堆临时文件越早处理概率越高。另外如果你用的是带本地历史功能的编辑器比如各类 IDE 的 Local History撤销掉那次删除往往比从文件系统恢复更快。2.5 IDE 本地历史与 Git 的双保险思路顺带提一个我踩过坑才养成的习惯。有一次我在 IDE 里改了一大段代码没保存就误触了撤销等反应过来的时候 git 里也查不到因为压根没提交过。后来才发现那个 IDE 自带 Local History 功能右键文件就能看到按时间排列的历史版本直接回滚就救回来了。所以我的双保险是这么搭的提交频率保持在高位哪怕是一个小改动先git add暂存起来也行暂存区里的内容同样能用git restore或git diff --cached找回来编辑器本地历史不要关它是 git 覆盖不到的那部分未保存、未跟踪的补充重要改动前先 stash 一份# 把当前所有改动打包存起来工作区变干净随时能取回 git stash push -u -m 调试前的备份 # 需要的时候看一眼列表再取回来 git stash list git stash pop-u的意思是连未跟踪文件一起 stash这一点很关键否则git stash只保护已跟踪文件的改动新建的文件还是裸奔状态。3. 提交写歪了--amend 与 reset --soft 的适用边界一旦内容进了本地仓库回退就从内容层面的恢复升级成了历史层面的修改。这时候最关键的分水岭就是这条提交推没推出去。没推出去的历史是你自己的私人草稿随便改推出去了的历史是公共契约动它就要付出沟通成本。3.1 amend 的本质是替换提交不是修改提交很多人以为git commit --amend是在原提交上打补丁这个理解会带来麻烦。它的真实行为是丢弃当前这条提交用新的内容生成一条全新的提交然后把分支指针指过去。新提交的哈希值和旧提交完全不同旧提交则变成游离状态靠 reflog 还能找到一段时间。理解了这个本质很多困惑就自动解开了。比如为什么 amend 之后必须强制推送——因为远端的提交和本地的提交哈希对不上普通推送会被拒绝git 认为你在丢失历史。再比如为什么两个人基于同一条提交并行工作时一方 amend 会导致另一方拉取冲突——因为那条提交在对方那里还存在但在你这里已经换了个身份。# 只是想把提交信息写清楚一点用 -m 直接覆盖 git commit --amend -m 修复订单金额计算时未考虑优惠券叠加的问题 # 漏了文件先补进去再用 --no-edit 保留原信息 git add src/order.js git commit --amend --no-edit # 只想改作者信息 git commit --amend --authorSan Zhang zhangsanexample.com--amend有一个隐藏的加分项它不会丢失原提交的提交时间以外的元信息作者和提交日期会被保留。这在补交代码的场合很有用不会出现明明是三天前写的代码提交时间却显示今天这种尴尬。3.2 漏文件、写错信息、想拆提交三种改法的对照同样是提交写错了用哪条命令取决于你想改成什么样。我整理了三种最常见的诉求和对应做法诉求做法关键点少加了文件git add .git commit --amend --no-edit记得补完文件再 amend提交信息写错git commit --amend -m 新信息只改信息不动内容一次提交想拆成两次git reset --soft HEAD~1后重新分批 add改动全部退回暂存区两次提交想合成一次git rebase -i HEAD~2选squash交互式变基会改写历史想删掉中间某次提交git rebase -i里把该行改成drop后续提交哈希全变其中--soft是我最推荐新手先掌握的因为它永远不会丢东西。它的效果是分支指针退回上一个提交那一次提交的内容原封不动地躺在暂存区里。你接下来可以重新挑选文件、分两次提交也可以改完信息再提交一次节奏完全由你控制。# 退掉最近一次提交改动全部留在暂存区 git reset --soft HEAD~1 # 看一下暂存区里都有什么 git status git diff --cached --stat # 重新组织成两次干净的提交 git restore --staged src/order.js git commit -m 重构订单金额计算逻辑 git add src/order.js git commit -m 补充优惠券叠加的单元测试对比一下--mixed默认模式它会把改动退回工作区也就是暂存区被清空了。如果你只是想重新挑选要提交的文件--soft更顺手因为文件还在暂存区你可以直接从中挑一部分撤出来而不是从头 add 一遍。3.3 敏感文件和大文件混进提交里的处理顺序这是回退场景里最需要小心的一类。配置文件里硬编码了密钥、日志文件、几 MB 的二进制包被顺手git add .提交上去了。这时候心里要有数--amend只对最近一次提交有效如果这个文件是三次提交前加进去的amend 帮不了你。处理顺序我建议这么走。第一步先判断这个提交推没推出去。没推出去用交互式变基处理# 打开最近 5 次提交的编辑界面 git rebase -i HEAD~5在打开的编辑器里把包含敏感文件的那一行前面的pick改成edit保存退出。git 会停在那个提交上这时你可以# 把文件从这次提交里剔除但保留在磁盘上 git rm --cached config/secret.yml git commit --amend --no-edit # 继续剩下的变基流程 git rebase --continue第二步如果这条历史已经推送到公共分支了那么必须假设密钥已经泄露。因为即使你后来改写了历史别人本地的仓库、CI 的缓存、各种自动构建产物里都可能还留着那份内容。正确的动作是先去服务端把这个密钥轮换掉再考虑要不要清理历史。第三步如果混进去的是大文件导致仓库体积暴涨要提醒自己一句回退提交并不会让仓库变小。因为旧的对象仍然存在于.git/objects里只是因为更早的历史引用不到了要等到垃圾回收才可能被清理。真要彻底瘦身得用git filter-repo这类工具重写全部历史再让所有人重新克隆。这是个大工程值得不值得做要提前和团队算清楚。3.4 amend 之后为什么要用 --force-with-lease一旦改写了已经推送的提交普通git push会被拒绝错误提示大概是Updates were rejected because the tip of your current branch is behind。这时候很多人第一反应是加-f# 不推荐不管远端现在是什么状态直接覆盖 git push -f origin feature/order # 推荐远端和我上次看到的一致才允许覆盖 git push --force-with-lease origin feature/order--force-with-lease的作用是加一道校验git 会检查远端分支当前指向的提交是不是你本地记录的远端跟踪分支的位置。如果一致才推送说明这期间没人往这个分支推过东西。如果不一致说明有人在你之后提交了这时候推送会被拒绝你就避免了一次把别人的提交覆盖掉的事故。我个人的使用原则很简单个人分支上想怎么改都行但要强制推送时一律用--force-with-lease从不用-f。这个习惯帮我避免过至少两次覆盖掉同事提交的惨案。至于共享分支比如 main、release我的建议是压根不要强制推送哪怕流程慢一点也别去碰。4. 已经推到远端revert 才是公共分支上的正确姿势到了这一层思路要换一下在共享分支上历史是只增不改的。你没法让所有人把已经拉下来的提交忘掉所以与其试图擦掉一笔不如大大方方地补一笔撤销。这就是git revert的设计初衷。4.1 revert 为什么是加一笔而不是擦一笔git revert commit做的事情是计算目标提交引入的变更然后生成一条新的提交把那些变更反向应用一遍。原来的提交还在历史里新的提交相当于它的逆运算两者一正一反相互抵消。分支指针继续往前走历史是一条完整的直线谁也不会因为你的操作而需要重新拉取。拿一个具体例子看。假设你在三次提交前改错了一个配置项导致线上服务频繁重启# 先看一眼历史确认要撤销的是哪一条 git log --oneline -10 # 撤销那一条提交git 会自动生成一条Revert ...的新提交 git revert a1b2c3d # 想撤销连续的几条用区间 git revert HEAD~3..HEAD这里有个很实用的细节git revert默认每撤销一条就立刻生成一条提交。如果你要撤销好几条中间又相互有依赖一条条提交会让历史很难看。这时候加上--no-commit参数让 git 把反向变更都堆到暂存区里最后你自己写一条提交信息一次提交git revert --no-commit HEAD~3..HEAD git status git commit -m 撤销错误的缓存在线配置回滚到稳定版本我更偏爱这种做法因为它留下的历史更干净后人看 log 时能一眼看懂这是一次整体回滚而不是三条不知所云的 Revert 记录。4.2 revert 撞上冲突的完整处理链路revert 不是每次都能顺利走完尤其是撤销的提交距现在比较远、中间又有其他改动碰过同一批文件时冲突几乎必然出现。这时候千万别直接git revert --abort跑掉更别去删.git目录老老实实按冲突流程走# 假设这时 git revert 报了冲突 # 第一步看清楚哪些文件冲突了 git status # 第二步打开文件找到 标记 # 按业务逻辑决定保留哪边注意这里要保留的是撤销后的结果 # 第三步把解决完的文件标记为已解决 git add src/config/cache.yml # 第四步继续 revert 流程git 会带你写提交信息 git revert --continue中间如果想放弃用git revert --abort回到操作前的状态这个命令是安全的它会完整还原。如果某一条撤销的内容已经被别人撤销过了revert 会提示没有东西可撤这时候用git revert --skip跳过去就行。我在实际处理冲突时有个小习惯先执行git revert --no-commit这样冲突解决完、add 完之后我还能最后通读一遍git diff --cached确认整体方向对不对再自己写提交信息。比让 git 自动生成信息要踏实得多因为自动生成的信息往往只写了Revert xxx没有说明为什么要撤销。4.3 撤销合并提交时 -m 参数怎么选这是 revert 里最容易卡住的地方。合并提交有两个父提交git 不知道该沿哪一条主线去计算反向变更所以会直接报错要求你用-m指定主线# 先看清楚合并提交的两个父提交 git log --oneline --graph -5 # -m 1 表示以第一个父提交为主线通常是你合并进的那个分支比如 main git revert -m 1 merge-commit-sha-m 1里的数字指的是父提交的序号。第一个父提交是合并时你所在的那个分支比如 main第二个父提交是被合并进来的那个分支。绝大多数情况下你要选的是-m 1因为你的目的是把这次合并带来的改动整体撤掉回到合并之前 main 的样子。选错主线会怎样git 会把被合并分支的变更当成新增内容重新应用一遍结果就是撤销反而把改动又加了一次。所以执行前一定要用git log --graph或图形化工具把父子关系看清楚别凭感觉选。还有一个后面一定会遇到的坑撤销了合并提交之后如果以后想把那次合并的代码重新合进来git 会认为这些提交已经在历史里了直接合并是合不进来的。解决办法是先把那次 revert 也撤销掉也就是下面要讲的撤销撤销然后再重新合并。4.4 撤销撤销把 revert 掉的内容再恢复回来场景很常见上线后发现新版本有严重问题快速 revert 回了旧版本过了两天问题修好了想把新版本重新放上去。这时候不能简单再 revert 一次那个 revert 提交虽然听起来绕但这就是正确做法而且 git 帮你处理得很好。# 找到那条 Revert 提交的哈希 git log --oneline -10 # 比如输出里有f9e8d7c Revert 上线新的推荐算法 # 撤销这条 Revert内容就回来了 git revert f9e8d7c因为 revert 的本质是生成逆变更那么对逆变更再做一次逆向结果就是恢复原状。这也是为什么我在做回滚时一定要在提交信息里写清楚为什么撤销因为一周后接手的人很可能就是你自己需要知道当时撤销的原因才能判断现在该不该恢复。有一点必须提醒revert 记录会永久留在历史里包括撤销的原因和当时的判断。所以提交信息别写回滚一下先撤了这种让人摸不着头脑的话写清楚撤销 v2.3 的缓存改造因为命中率下降导致数据库压力升高半年后看 log 的人会感谢你。5. reflog所有手抖事故的最后一道保险前面几节讲的都是有计划的回退这一节讲的是出事故之后的急救。reflog 是 git 里最被低估的功能它的存在让绝大多数严重误操作都变成了可恢复的。但前提是你要知道它怎么用以及它的保鲜期有多长。5.1 reflog 到底记了什么能留多久git reflog记录的是HEAD 指针的每一次移动。不管你是提交、切换分支、reset、rebase、merge 还是 amend只要 HEAD 动了就会留下一条记录格式大概是这样的git reflog # a1b2c3d (HEAD - main) HEAD{0}: commit: 修复订单金额计算 # f9e8d7c HEAD{1}: reset: moving to HEAD~1 # b8c7d6e HEAD{2}: commit: 上线新的推荐算法 # 3d4e5f6 HEAD{3}: checkout: moving from feature to main左右两列分别是提交哈希和这个位置在几次移动之前。HEAD{1}就是上一次操作前的状态HEAD{3}是三次操作前。这个编号就是你的时间旅行入口。保鲜期这块要记清楚默认情况下可达的 reflog 条目保留 90 天不可达的已经变成游离状态的提交保留 30 天之后会被 git 的自动垃圾回收清掉。这两个值由gc.reflogExpire和gc.reflogExpireUnreachable控制。也就是说出了事故之后大约有一个月的时间窗能救回来但这一个月里如果你手工跑了git gc --prunenow那可能当场就没了。提示出事故后最忌讳的操作是继续乱敲命令。每次 HEAD 移动都会在 reflog 里多一条记录把真正的目标位置越推越远反而增加恢复难度。5.2 被 reset --hard 冲掉的提交怎么找回来这是 reflog 最经典的用武之地。假设你刚敲了git reset --hard HEAD~2两条提交连同里面的改动一起消失了。别慌按这个顺序来# 第一步看 reflog找到 reset 之前的那条记录 git reflog # 假设看到a1b2c3d HEAD{1}: commit: 补充优惠券叠加的单元测试 # f9e8d7c HEAD{2}: commit: 重构订单金额计算逻辑 # 第二步有两种走法选一种 # 走法 A直接把分支指针挪回去适合确认要完整回退 git reset --hard a1b2c3d # 走法 B先建一个临时分支保护现场看完再决定 git branch rescue/a1b2c3d a1b2c3d git log --oneline rescue/a1b2c3d -3我强烈推荐走法 B。因为reset --hard是第二次使用同一个高危命令万一目标选错了你就得再来一轮 reflog 考古越搞越乱。先建分支的好处是那条提交被分支引用住就再也不会被垃圾回收清掉你有了充足的时间慢慢检查内容。确认无误之后再把当前分支挪过去或者从那个分支里cherry-pick需要的提交。还有一个更省事的办法如果你只是想撤销刚刚那次 reset直接git reset --hard ORIG_HEAD就行。git 在执行 reset、merge、rebase 这类动作前都会把原位置写进ORIG_HEAD相当于一个自动的一次性备份点。不过它只记最近一次要是中间又敲了别的命令就得回到 reflog 里找了。5.3 删掉的分支和 dangling 对象fsck 兜底还有一种更彻底的情况分支被删了而且当时没记哈希值。比如git branch -D feature/payment这个分支上的提交如果没被其他分支或标签引用就会变成游离对象。如果删除动作刚发生不久reflog 里通常还能看到# 分支删除也会在 reflog 里留痕如果删之前 HEAD 在上面 git reflog # 找到那个提交之后直接用哈希重建分支 git branch feature/payment b8c7d6e如果 reflog 里已经翻不到比如隔了很久或者当时根本没在那个分支上操作就轮到git fsck上场了# 找出所有没有被引用的对象 git fsck --unreachable # 更直接的做法让 git 把游离对象归拢到一个目录里 git fsck --lost-found执行完第二条git 会把找到的游离提交写到.git/lost-found/commit/目录下每个文件的名字就是提交哈希。接下来逐个查看内容# 看这条游离提交改了什么 git show $(ls .git/lost-found/commit/ | head -1) # 确认是想要的那条之后给它挂个分支名正式救活 git branch rescue/from-fsck sha这个手段我用了大概三次两次救回来分支一次没救回来——那次是误操作隔了两周才发现中间机器上跑过手工的 gc。所以我的结论是发现越早成功率越高。养成删分支之前先看一眼git log --oneline -1记下哈希的习惯比事后来考古要轻松一百倍。5.4 出事之后的第一反应决定你还能不能救回来把急救流程浓缩成几条动作贴在显示器边上都不过分第一立刻停止在仓库里做任何写操作包括提交、切换分支、reset、pull。所有这些都是往 reflog 里加新记录会稀释掉你真正想找的那条。第二先把仓库整个目录复制一份备份。这一步几乎零成本但能在你后续操作再次失误时保住最后的希望。仓库大的话只备份.git目录也行。第三用只读命令去看git reflog、git log --all --oneline、git fsck --unreachable、git show sha、git diff sha HEAD。这些命令都不会改变任何状态可以放心大胆地试。第四找到了就用分支挂起来别急着 reset。分支是一个稳定的引用点挂上去之后就算再折腾几天也不会丢。最后提醒一句关于运行环境的事这些操作全部依赖本地.git目录里的对象。如果项目是克隆到临时容器、临时构建环境里的容器一销毁reflog 就跟着没了。真正重要的东西能提交就提交能推送就推送别指望靠 reflog 长期兜底。6. 几类高频报错与真实踩坑复盘前面讲的是怎么做这一节讲的是为什么做了却没生效。回退这件事最容易卡住人的地方往往不是命令本身而是环境、配置和工具链带来的干扰。6.1 从无法将 git 项识别为 cmdlet说起在 Windows 上很多人的第一次失败不是回退失败而是 git 命令压根跑不起来。典型报错是git : 无法将git项识别为 cmdlet、函数、脚本文件或可运行程序的名称或者fatal: not a git repository之类。前者是环境变量的问题后者是工作目录的问题两者经常被混在一起。排查顺序我一般是这样走的。先确认 git 到底装没装、装在哪在开始菜单里搜一下有没有 Git Bash有的话直接打开它能执行git --version就说明装成功了。然后在普通命令行里再试一次如果 Git Bash 里能用、PowerShell 里不能用那基本可以确定是 PATH 环境变量没配好。最省事的修复方式是重装一遍 Git for Windows在安装向导里选Git from the command line and also from 3rd-party software这一项它会把 git 的可执行目录写进 PATH。装完之后必须彻底关闭并重新打开终端包括 IDE 里的内置终端否则它继承的还是旧的环境变量。还有一种更隐蔽的情况PATH 配好了命令也能跑但每次都要敲完整的git.exe。这通常是因为 PATH 里被塞了多个 git 路径比如以前装过便携版又装过正式版优先级乱的。打开系统环境变量把路径列表清一清只留一个有效路径就行。6.2 fatal: not a git repository 的三种常见成因这个报错的意思是当前目录及其所有上级目录里都没有找到.git排查的时候按这三个方向想第一种目录不对。你在执行命令时所在的目录不是仓库根目录或它的子目录。这种情况最常见的触发场景是终端上一次cd到了别的地方然后切回来忘了。先用pwdWindows 用的是Get-Location确认当前路径再cd到项目目录里重试。第二种.git目录真的没了。可能是清理磁盘时误删或者是用了某些清理大文件的工具把隐藏目录一起清了。这种情况下 git 命令全部失效但代码文件本身还在。别慌还有救如果远端有完整历史直接重新克隆一份再把当前工作区的改动拷过去用git diff --no-index逐一比对。如果远端也没有那只能靠文件系统层的恢复手段或者看看有没有备份。第三种这个目录是子模块或者被嵌套了。在子模块目录里执行 git 命令看到的历史和主仓库不一样很多人会误以为操作没生效。用git submodule status看一眼就能确认。顺带说一个和部署相关的提醒发布产物里不要带上.git目录。它在开发环境是保命的东西在线上环境就是信息暴露面把整个提交历史和分支信息都摆在公开目录下了。构建脚本里加一行排除规则几秒钟的事。6.3 恢复之后代码看起来全是改动换行符与文件权限位这个坑我踩过一次印象很深。当时用git checkout恢复了几个文件结果git status一看几十个文件全标成了已修改git diff里每个文件都是整篇红绿。第一反应是恢复错了分支排查半天才发现是换行符在捣鬼。原因是这样的Windows 上默认的core.autocrlftrue会让 git 在检出时把 LF 转成 CRLF提交时再转回 LF。如果一个团队里有人机器上开着这个配置、有人关着同一份文件在不同机器上就会来回被标记成改动。解决方案是用.gitattributes把规则写进仓库而不是依赖每个人本地的配置# 在仓库根目录建 .gitattributes * textauto eollf *.bat text eolcrlf *.png binary这样所有人不管本地core.autocrlf怎么设检出和提交的行为都是一致的。另外执行恢复命令时临时关掉也行git -c core.autocrlffalse checkout -- src/。同类问题还有文件权限位。在 Linux 或 macOS 上共享目录、或者跨系统挂载时文件的可执行位可能莫名其妙地变了导致git diff显示old mode 100644 / new mode 100755。这种改动内容其实是空的只是权限位不同。处理办法是# 让 git 忽略权限位变化 git config core.filemode false # 如果已经有文件被误标了权限从索引里修正 git update-index --chmod-x path/to/file这两个坑的共同特点是它们造成的大量改动都是假的千万别顺手git add .再提交一次。一旦提交进去整个仓库的历史里就多了一堆无意义的 diff后面再想清理会很痛苦。6.4 命令行回退与图形化工具的对照以及我的几条操作习惯用不用图形化工具看个人但至少要知道它们对应的是哪条命令否则出了问题无从排查。以常见的图形化客户端为例右键某条历史记录菜单里的重置到此版本通常有三个子选项——软、混合、硬正好对应git reset --soft、--mixed、--hard还原此变更对应的是git revert编辑提交信息对应git commit --amend。理解了命令的含义用图形化反而更直观因为你能看到提交图谱不容易选错主线。另外还有一个细节可以解释很多人的疑惑。如果你在 IDE 的输出窗口里看到过这样的命令git -c diff.mnemonicprefixfalse -c core.quotepathfalse --no-optional-locks status别以为是什么异常调用。这是 IDE 在替你执行 git前面几个-c只是临时改了 diff 前缀的显示方式、让中文路径正常显示、避免和命令行工具抢占索引锁。它不影响你的仓库内容--no-optional-locks这个参数本身就是为了让它并行运行更安全一些。最后说几条我自己踩过坑之后固化的习惯都是在真实事故里换来的动手前先备份。涉及reset --hard、clean -fd、rebase、强制推送这几类操作时先git branch backup/$(date %m%d)建个临时分支。成本是一条命令收益是任何操作都能一键回到原点。写操作前先想清楚退到哪一层。是工作区、暂存区还是仓库指针。三层想清楚了命令自然就选对了根本不用背。提交信息写清原因而不是动作。回滚一下和撤销 v2.3 缓存改造因命中率下降压垮数据库这两条信息在未来某个深夜排查问题时价值差得不是一星半点。公共分支上只用 revert个人分支上才用 reset。这条界线守住就不会有把同事的提交覆盖掉这种需要赔礼道歉的事故。发现出事了第一反应是停手看 reflog而不是继续敲命令。reflog 给了你大约一个月的时间窗但每多敲一条写命令这个窗口就远一点。多数时候冷静十秒钟比手速快重要得多。