
1. git回退到底在“退”什么先把三个区搞明白很多人在git里一听到“回退”就头疼其实90%的混乱都来自同一个地方没搞清楚git到底有哪几个存放代码的“区域”。我想先花点时间讲这个因为只有把结构弄明白后面的reset、revert、restore才不是死记硬背。git从工作到提交代码要经过三个区工作区Working Directory、暂存区Index / Staging Area、仓库Repository也就是本地commit记录所在的.git目录。你可以把工作区想象成你的办公桌文件改到一半扔在上面暂存区是待发货的快递箱你把要寄的文件放进去随时可以决定寄不寄仓库就是已寄出的包裹记录每一次commit都是一条不可磨灭的快递底单。git add把文件从办公桌放进快递箱git commit把快递箱里的文件打包寄出并生成底单。很多人以为回退就是“把快递追回来”但git里回退的方向不止一个有的退的是快递底单commit记录有的退的是快递箱内容暂存区有的退的是办公桌文件工作区。这三者一旦混在一起命令自然就晕了。还有一个核心概念HEAD指针。git里有一个叫HEAD的指针它指向你当前所在的提交记录。每次commitHEAD就往前挪一格。回退命令的本质就是把这个指针往过去的方向移动或者让某个提交记录“反向操作”一次。理解了HEAD你就理解了reset和revert的区别——reset是真的让指针往后走revert则是让指针往前走同时把过去那个提交的改动抵消掉相当于“吃了后悔药但时间线继续前进”。除此之外还要知道git里有四个地方保存着你的“版本状态”工作区、暂存区、HEAD提交记录以及“丢掉的提交记录”所在的reflog操作日志。后面讲找回误回退的代码时会用上reflog这里先记住一个词只要git没垃圾回收大多数误操作都还有救。2. 回退命令全拆解reset、revert、checkout/restore、amend网上搜“git回退命令”最常见的就是git reset和git revert但真正实操起来很多人连git checkout和git restore也分不清。这一节我把每个命令掰开揉碎告诉你它到底动了哪个区什么场景该用哪个。2.1 git reset版本库位移的三档模式git reset本质是移动HEAD指针还可以顺带改变暂存区和工作区。它有三个模式面试和日常都最爱问--soft只移动HEAD指针工作区和暂存区都不动。也就是说你上一次commit的文件仍然留在暂存区里随时能重新commit。适合你发现上次提交漏了文件、commit信息写错时把提交“撤回”成“已add未commit”的状态然后重新来。--mixed默认移动HEAD指针同时把暂存区重置为HEAD所在位置但工作区不动。意思是commit记录退回上一条暂存区清空但文件内容还是你改过的样子。这时候文件显示为“已修改未暂存”。--hard移动HEAD指针暂存区和工作区全部重置。这是真正意义上的“时光倒流”会把工作区里所有未提交的改动一并抹掉一旦执行非reflog级别的恢复比较麻烦。举个例子当前HEAD指向commit C你执行git reset --hard HEAD~1HEAD就回到commit BC提交彻底从分支历史里消失C里的文件改动也会消失工作区变成B的状态。就像你把一本杂志撕掉最新一页还顺带把办公桌上所有相关草稿都扔了所以--hard是三个模式里最危险的。2.2 git revert安全回退的提交记录方式git revert就不一样了。它不会把HEAD往回拉而是在当前HEAD的基础上生成一个新的提交这个提交把之前的某个提交的改动反着做一遍。比如你提交了“加了横幅”revert就提交一个“删掉横幅”历史里两条记录都在代码效果回到加横幅之前。为什么要有revert因为reset会改写历史一旦提交已经push到远程仓库其他人也拉下了这条记录你贸然reset再push就会造成别人的分支和你的不一致报”非快进“错误。而revert是追加正向操作不会动历史其他协作者pull下来也不会冲突。适合场景已push到共享分支、或者不想修改历史记录时用revert。代价是提交记录里会多一条revert记录有人觉得不够干净但这是保命写法。2.3 git checkout / git restore文件级撤回与分支切换的分工git 2.23之前git checkout很霸道既管分支切换又管文件恢复。2.23之后git听劝了拆出两个新命令git switch只管分支git restore只管恢复文件。不过大量旧习惯还在用checkout你要能看懂。git checkout -- file表示把工作区里某个文件恢复到暂存区或HEAD里的版本作用是“我不要这次改了”也就是丢弃工作区改动。注意这个操作同样危险没commit的改动直接没了。git restore file本质一样是checkout的现代替代。git restore --staged file则是把文件从暂存区“撤下来”但保留工作区改动相当于git reset HEAD file。需要区分的是git checkout branch是切换分支git checkout commit -- file是从某个提交里把文件捞出来。不加--时git会先当“切换分支”解释加了--明确告诉它是“恢复文件”这个细节很关键。2.4 git commit --amend修改最后一次提交严格说--amend不算回退但它经常被和回退一起讨论因为它的作用是“替换掉最后一次提交”。比如你commit后发现漏了一个文件或者commit message写错字可以用它把新改动和原提交合并成一个新提交。我见过最经典的误用是在已经push过的提交上执行git commit --amend然后push失败。原因很简单amend本质是撤销旧的提交、生成一个新的提交历史被改写了远程的旧提交无法“快进”到新的提交。这种情况要么push时加--force-with-lease强烈建议加lease保护要么就别对已共享的提交使用amend。下面这张表可以帮你快速对号入座场景推荐命令是否改写历史是否动工作区撤销最近一次commit保留改动在暂存区git reset --soft HEAD~1是否撤销最近一次commit改动回工作区git reset --mixed HEAD~1是否彻底回退并丢弃所有未提交改动git reset --hard commit是是撤销已push的提交保留历史git revert commit否否丢弃工作区某个文件的改动git checkout -- file/git restore file否是从暂存区撤下某个文件git restore --staged file否否修改最后一次提交信息或补文件git commit --amend是否3. 实操走一遍从误提交到安全恢复理论知识说再多不如动手跑一遍。这一节按我实际处理过的几个典型场景走完整流程命令可以直接抄。3.1 场景一刚commit后发现提交错了这种情况下commit还没push历史只有你自己看过用reset最顺手。假设你刚执行了git commit -m feat: 添加登录功能但马上发现漏了config.js这个文件没加进去。执行git reset --soft HEAD~1HEAD~1表示HEAD的上一个提交。这时候HEAD已经回到commit之前但config.js如果之前add过它还在暂存区没add过就还在工作区。你重新补上git add config.js git commit -m feat: 添加登录功能如果你的本意是“上次提交信息写错了重新写”也可以git commit --amend -m feat: 登录功能完整版不过注意--amend会替换上一次提交如果上一次提交里有别的内容其实你只是在原提交基础上改message不会丢改动。3.2 场景二已经push到远程后回退如果提交已经push到远程且不是你一个人在用我强烈建议用revert而不是reset。假设刚刚push了一个有bug的提交commit id是abc1234git revert abc1234git会打开编辑器让你填revert commit的message保存后自动生成一个新提交。这时再pushgit push同事pull下来只会看到你多了一条“Revert abc1234”的记录他们的本地分支不会出现历史分叉。如果哪天你想把“撤销的撤销”也做回来可以git revert那个revert提交等于是重新引入了原来的改动非常方便。但如果你确定远程仓库只有你一个人用同事也不会乱拉代码那就也可以用reset。执行完git reset --hard 目标commit git push --force-with-lease--force-with-lease是个好习惯它会在强制推送前检查远程分支是否还停留在你上次拉取的状态如果别人已经push了新代码它会拒绝防止你把别人的提交冲掉。3.3 场景三只想撤销某个文件不想动其他提交有时候不是想回退整个提交只是觉得“这个文件改砸了我想回到昨天版本”。这种情况千万不要reset直接文件级操作git restore file # 或者老一辈写法 git checkout -- file如果你只把文件add进了暂存区但还没commit执行git restore --staged file注意这两条命令不会影响其他文件的改动也不会影响提交历史安全得多。如果你相对某个具体提交来恢复这个文件比如从commitdef5678里取这个文件git restore --sourcedef5678 -- file--source就是告诉git“我从这个提交里捞文件”非常实用。3.4 场景四回退后想把丢掉的提交找回来这是很多人的噩梦执行了git reset --hard然后发现回退过头了原来的代码没了。别慌reflog就是后悔药。git reflogreflog会列出你本机所有HEAD指针移动过的历史每一条都有对应的commit id。比如输出里有这么一行abc1234 HEAD{3}: commit: feat: 登录功能找到你丢的那个提交id然后执行git reset --hard abc1234提交记录和文件就都回来了。reflog默认会保留30天左右所以误操作后第一件事是别乱关终端马上用reflog查。另外提一点reflog只记录本地操作不会同步到远程所以你换台电脑是查不到的。3.5 场景五分支合并后回退热词里有人搜“git分支合并”我就把回退和合并的痛点一起说。假设你把feature分支合并进main合并后发现问题想退掉这次合并。如果是刚merge完还没pushgit reset --hard ORIG_HEADORIG_HEAD是git在merge、reset等危险操作前自动保存的“之前HEAD位置”用来快速回滚。但如果已经push了你遇到的是典型的“合并commit有多个parent”的问题git revert -m 1 合并commit_id可以指定保留主线main分支把被合并分支的改动给撤销掉。git revert -m 1 merge_commit_id-m 1的意思是保留第1个parent也就是合并前的main分支状态。这个参数不填的话revert会不知道“到底该回到合并前还是只撤销某个分支”所以会报错很多新手卡在这个地方。4. 常见问题与排查技巧实录4.1 “fatal: not a git repository”这类报错的真相热搜词里有fatal: not a git repository (or any of the parent directories): .git这个报错经常出现在执行git reset、git commit时。原因是你当前所在的目录不是git仓库或者git找不到.git目录。我在实际使用中遇到的90%情况是进错了目录。比如在/home/user下执行git命令但git仓库实际在/home/user/project。检查方式先执行pwd看当前路径再用ls -a看有没有.git目录。如果确实是一个git仓库但好奇为什么找不到可以执行git rev-parse --git-dir它会告诉你git认为的仓库目录是哪。还有个比较隐蔽的情况在某些IDE的终端里默认启动目录被设置为项目之外的路径很多人明明打开了项目却报这个错最后发现是终端目录不对。4.2 ssh认证失败、clone失败和回退找不到目标热词里有“ssh认证失败 git”虽然这句主要出现在clone/push时但和回退也有关系当你push回退结果时如果认证出问题就会卡在最后一步。ssh认证失败的排查路径很固定ssh -T gitgithub.com如果返回“Permission denied”优先看密钥对不对~/.ssh/id_ed25519.pub有没有添加到你账户的SSH keys里。如果返回的是其他错误比如“Host key verification failed”直接删除~/.ssh/known_hosts里对应的那行再重新连接一次即可。还有一类是“git error: src refspec master does not match any”这出现在reset后push时意思是本地分支里根本找不到你要推的提交。多半是git reset --hard把历史退到了某个旧提交而本地当前分支不再包含远程分支的某些提交换个说法你当前分支的HEAD跟远程分支对不上了。这时候确认分支名很重要常见的是本地分支叫main远程分支叫master你推错名字当然找不到。4.3 回退后代码文件不翼而飞、reflog恢复上面已经提过reflog我再补充两个实际经验。第一reflog不是无限期的默认的回收期和gc设置有关想保险一点可以把gc.reflogExpire设为never但我不建议因为reflog会持续变大影响性能。个人做法是重要项目每周做一次巡检。第二git reset --hard执行前最好先确认你当前的工作区没有未提交改动因为--hard会把它们全部丢弃。真要硬来的话建议先打个临时stash或者复制一份文件到系统临时目录cp -r project project_backup这句话不贵但能救命。我现在已经养成了习惯凡是涉及--hard的重度操作先备份整个项目目录到/tmp或者桌面操作完再删。4.4 git免密配置、credential清除账号密码的小坑热搜里有“git免密”和“git清除账号密码”跟回退push成功后不想反复输密码有关。我推荐用SSH密钥方式而不是记住HTTP密码原因是SSH密钥可以绑定不同主机且不会明文存在配置里。如果是HTTP方式凭据被缓存了想清除密码的方式因系统而异。Windows下常见的是用“凭据管理器”删除git条目macOS下则和钥匙串相关。直接命令行方式git config --global credential.helper git credential reject第一个命令是临时关闭凭据存储第二个是手动拒绝已保存的凭据。注意Windows下有些版本默认用的credential manager直接git config --global --unset credential.helper就能重置。4.5 git commit --amend的常见误解为什么别人说“请勿amend已push的提交”再补充一个高频问题。很多人以为git commit --amend只是改个提交信息不会动其他东西所以在共享分支上直接用了然后push时报“error: failed to push some refs”。原因已经很清楚了amend会生成一个全新的commit对象旧的提交对象虽然还在但已经不属于当前分支了。历史被改写后远程分支和本地分支形成分叉git不会允许你直接快进推送。解决方案就是前面说的--force-with-lease但要注意如果同事已经基于旧commit拉过分支改过代码你强制推送会让他们的本地历史也发生分叉处理起来很麻烦。所以我的建议是凡是你会发给别人看的提交都不要amend也不要reset。只有确保“只有我一个人看过这个提交”时才用这些会改写历史的命令。否则老老实实revert。最后说一个我踩过不少次坑后的体会git回退命令的难点从来不是命令本身记不住而是你要时刻清楚自己正在操作哪个“区域”。每次执行回退前先在脑海里过一遍这个命令会把HEAD移到哪会不会动暂存区会不会动工作区想清楚了再敲回车基本上就不会把代码搞丢。我电脑上现在都习惯性挂一个alias把git reset --hard设置成必须二次确认的形式防止手滑。这个小习惯分享给你关键时刻真的能救回一个加班的夜晚。