我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Git版本回退与撤销:reset、revert、restore、amend命令全攻略

Git版本回退与撤销:reset、revert、restore、amend命令全攻略 1. 版本回退的核心思路为什么Git能穿梭时空1.1 开发中的三种后悔场景做开发的都知道写完代码那一刻的兴奋劲往往撑不过三分钟。紧跟着来的就是各种版本的哎呀刚才那次提交把配置文件一块儿带上去了里面居然写着本机测试账号或者提交信息手滑打成了fix bug实际上改了十几个文件下次看log根本不知道当时在想啥更惨的是闷头写了两小时提交完一跑测试发现整个思路就是错的得回到动手之前的状态重来。这篇文章就专门解决这三类问题。Git作为目前最主流的分布式版本控制系统它的核心能力之一就是版本回退与修改撤销——说白了就是给你一颗后悔药。我会从四个高频命令入手git reset、git revert、git restore、git commit --amend把它们的原理、适用场景、实操手法和踩坑点一次讲透。不管你是刚装好Git、正在照着教程配置环境的新手还是已经在团队项目里做分支合并、代码评审的老手这篇东西都能让你少走弯路。先补一个基础概念方便后面统一语境在Git里每一次git commit之后当前仓库就多了一个快照这个快照有唯一的哈希值一般是40位十六进制平时我们看前7位就够。HEAD是一个指针指向当前所在的分支上最新的一次提交。版本回退的本质要么是移动HEAD和分支指针的位置要么是生成一条反向修改的新提交。搞懂这个底层逻辑后面所有的命令就不需要死记硬背了。1.2 四把后悔药的分工很多教程把回退和撤销混着讲结果读者越看越迷糊。我这里先用一张表把四个命令的定位分清命令操作层级是否改写历史典型场景git reset移动分支指针是本地提交后想撤销提交、取消暂存、丢弃工作区修改git revert新增一条反向提交否不删历史回退某个已推送到远程的提交多人协作git restore工作区 / 暂存区否未提交的修改要撤回不想动提交历史git commit --amend最近一次提交是刚才提交错了信息、漏了文件、改错了一行简单理解reset是时光倒流revert是补一条修正案restore是只擦没存档的修改amend是回炉重造上一次提交。这里的是否改写历史直接决定了你在单人分支和多人协作场景下能不能用。我见过不少人拿reset去回退远程分支上的提交结果push被拒然后一气之下用了--force把同事的代码直接干掉了——这种事故线上出过太多次了。所以后面每一节我都会提醒哪些操作只适合本地哪些操作推远程要慎重。2. git reset实操三种模式的选择2.1 soft模式只撤销提交修改原封不动留在工作区git reset --soft干的事儿特别纯粹把HEAD和分支指针往回拨到指定提交但是索引区暂存区和工作区的所有内容都保持不变。换句话说你上一次提交的所有文件变更会整整齐齐地待在暂存区里等着你重新提交。这个模式最有用的场景是提交信息写错了但内容一点问题没有。比如你敲了git commit -m fix login结果发现自己想表达的是修复登录页验证码逻辑或者干脆是把两个不相关的改动塞在了同一条提交里。这时候执行git reset --soft HEAD~1HEAD~1表示当前提交的父提交也就是回退一条提交。执行完再跑git status你会看到所有文件都处于Changes to be committed状态。接下来你爱怎么折腾怎么折腾重新git commit -m 正确的信息或者分开暂存、拆成多条提交。手动操作的时候要注意如果刚才的提交不是最新的那一条而是中间某条--soft回退到那里会把中间所有提交的改动全部塞回暂存区需要你自己一个个去挑拣这个动作叫整理暂存区配合git add -p可以做交互式选择。实操下来我是建议这种场景尽量用--soft而不是--hard因为hard一步到位把改动全丢了想找回来还得靠reflog太折腾。2.2 mixed模式撤销提交并取消暂存日常最常用git reset不带任何参数默认就是--mixed模式这也是Git命令行里最温柔的默认回退。它的行为是移动HEAD指针同时把暂存区的内容清空回工作区。也就是退回提交、保留改动、但不再暂存。跑完git status你会看到文件变成了Changes not staged for commit改动还在只是处于未暂存的普通状态。这个模式的典型场景是你提交了一大堆文件回头一看里面有个不该提交的配置文件混进去了或者你发现这次提交的范围太大了想把它们先解放出来重新整理。我自己的习惯是只要一条提交里混入了好几种不相关的改动就先git reset不带参数把所有变更退回工作区然后再一个个git add、分多次提交。虽然看起来多敲了几步但git log的清晰度不知道值回多少加班时间。有一点需要留意--mixed模式下工作区里你原有的未提交修改也还在不会被破坏。万一执行后发现有冲突头merge conflict的标记 出现说明之前有几方改动重叠了需要手动清理别慌这跟普通冲突处理流程一样。2.3 hard模式彻底回退用完记得看refloggit reset --hard可以说是Git世界里最狠的操作了。执行它之后HEAD指针、暂存区、工作区三者的状态全部回退到目标提交自那次提交以来的所有修改、新增、删除统统从当前分支消失。用起来极其顺手但风险也最高。什么时候才该用hard模式我的经验是只在明确要彻底丢弃某段改动的时候才用。比如实验性质的代码写坏了或者一整个功能方向被否了你想回到一个干净的状态重新开始。这时候git reset --hard HEAD~3意思就是回到最近3次提交之前的干净状态中间三条提交的成果全部不要了。注意是不要了不是暂时隐藏。删除的提交并不是立刻从磁盘上消失只要你的终端还开着、没有清理过用git reflog还能找回来。但如果你第二天关机重启reflog记录过一段时间会被清理那就真的可能找不回来了。这也是我不知道给多少人重复过的建议用--hard之前养成先跑git status和git log --oneline -5的习惯确认目标提交是你真正想回去的地方。别问问就是我见过有人敲成git reset --hard HEAD当场上演代码消失术。2.4 相对引用与精确回退HEAD~、HEAD^、提交哈希实际回退的时候你很少会只回退一步。Git提供了两种定位历史提交的方式相对引用HEAD~1、HEAD~5表示往前数几代HEAD^表示父提交只有一个父提交时与HEAD~1等价在合并提交上HEAD^指向第一父HEAD^2指向第二父绝对引用用完整的40位哈希或者写前7位够用的缩写比如git reset 3f9a2c1判断用哪种很简单回退一两步、且你知道是最近几次修改用相对引用回退很多步或要精确回退到某个特定的历史节点用绝对引用更稳妥。写相对引用的时候有个容易绕晕的细节HEAD~2指的是当前提交往前第二个祖先也就是说回退两步。如果你的分支上前两步之间有过mergeHEAD~2走的是主线的祖先链可能跟你想的不一样。碰到复杂历史我建议先git log --graph --oneline拉出提交拓扑图看清楚再动手。我再补一个更细的经验回退前最好先给当前状态打个tag比如git tag backup-before-reset。这样万一回退完发现不对git reset backup-before-reset就能一步回到回退前的状态比翻reflog找哈希省心得多。3. git revert实操面向协作的安全回退3.1 为什么多人协作时要优先用revert前面说的git reset本质是移动指针、删除历史。如果你已经把这个提交推送到了远程分支再在本地reset然后强制推送就会造成一个严重后果远程分支的历史被改写了其他同事本地基于旧历史的提交全部变成孤儿下次pull时一堆冲突等着他们。在团队协作里这是纪律性事故不是技术事故。git revert的思路完全不同它不动任何历史提交而是新建一个提交把目标提交的改动反向撤销。比如你之前提交了增加红色背景按钮revert之后会生成一条移除红色背景按钮的提交。git log里两条提交都在历史是线性增加的其他同事拉代码时不会察觉任何异常顶多是多了一条提交。所以在共享分支master、develop、release这些上我严格遵守的原则是只revert绝不reset --hard force push。reset留给本地个人分支怎么浪都行。3.2 revert的完整操作流程与冲突处理git revert的用法相当直接回退最近一条提交执行git revert HEAD回退指定的某条提交执行git revert commit-hash。命令敲下去之后Git会打开默认编辑器让你填提交信息默认信息已经写好了类似Revert fix: xxx这种格式一般直接保存就行。退出编辑器后一条反向提交就创建完了。不过实操中很少有这么顺的revert经常遇到冲突。原因是你要撤销的那次修改可能已经被后续的提交改写了。想象一下你删掉了config.js里的一行配置同事后来又在同一行附近加了新配置这时候revert想倒放你的删除就会撞车。Git会在冲突文件里标出和你需要手动判断保留哪些内容处理完git add再git commit完成revert。我处理revert冲突的小技巧是先看git status确认哪些文件冲突再git diff看看当前文件和revert目标提交的差异。多数情况下revert冲突的解法不是执行撤销而是只去掉目标提交的改动、保留别人后来的增量。想清楚这个语义冲突就好解决了——你并不是要让代码回到某一个旧状态你是要抵消某一笔订单的修改结果。3.3 回退合并提交revert -m参数这里有个坑必须单独拎出来讲。如果你要回退的是一个合并提交merge commit直接执行git revert merge-commit-hashGit会报错error: commit X is a merge but no -m option was given。因为merge commit有两个父提交Git不知道你想沿着哪条线走。解决办法是用-m参数指定主线git revert -m 1 merge-commit-hash-m 1表示保留merge提交的第一父通常是主线比如master把第二父被合并进来的特性分支上的改动全部撤销掉-m 2反过来保留第二父撤销主线的改动。绝大部分时候我们需要的是-m 1因为撤销这次合并带来的特性分支改动是主线需求。用过几次你就明白revert合并提交的语义比reset复杂得多甚至一些老手也会在-m的方向上栽跟头。我的建议是命令执行前先用git show merge-commit-hash --stat --parents看一眼两个父提交分别是谁确认-m 1和-m 2哪个才是你要的主线。4. 修改撤销实战restore与amend4.1 未提交的修改restore与checkout的替代关系日常开发中频率最高的撤销需求其实不是回退提交而是哎这行代码我刚写的是什么来着我想还原回去。这类操作不涉及提交历史只碰工作区和暂存区。在Git 2.23版本之前大家习惯用git checkout -- file来丢弃工作区的修改。后来Git官方觉得checkout这个命令的职责太多了就新增了git restore来专门干这件事# 丢弃工作区某个文件的未暂存修改 git restore file # 丢弃所有未暂存修改注意别误伤确认一下再动手 git restore . # 从暂存区拉回文件但不丢工作区改动即取消暂存 git restore --staged file参数拆解一下--staged表示操作对象是暂存区把暂存区里的文件还原为HEAD里的版本本质就是git reset HEAD file的替代写法。不带--staged时操作对象是工作区把工作区文件还原为暂存区里的版本。很多教程喜欢比较git restore和git checkout --实际体验下来restore的命令语义更清晰不容易跟切换分支的checkout搞混。新手我建议直接学restore老手也别死守旧习惯。唯一要注意的是restore操作无法找回被丢弃的工作区修改所以执行前可以考虑先副本备份或者用git stash把修改暂存起来——stash是临时保存现场之后随时可以恢复安全性高得多。4.2 已提交且未推送commit --amend修复上一条提交git commit --amend这个命令我愿称之为提交急救包。它允许你修改最近一条提交的内容和提交信息而不会生成一条新的提交记录。具体能力有三个第一修改提交信息。git commit --amend -m 新的、正确的提交信息执行之后最近一条提交的哈希会变化因为内容变了git log里看到的还是只有一条提交但信息已经是新的了。这个命令我几乎每天都会用有时候提交信息写完没检查错别字有时候想补充更详细的说明--amend改完就行。第二补充漏掉的文件。git add 刚才忘提交的文件 git commit --amend --no-edit--no-edit的意思是不要改变提交信息直接把刚才add的内容补进上一条提交。注意上一条提交的文件差异会变哈希也会变。只要这条提交还没推到远程这就是个完美的补救手段。第三修正代码内容。git add 修改过的文件 git commit --amend这个场景要小心使用。如果你在补充内容时跟后面提交的内容有前后依赖很容易把提交历史搅成一团浆糊。我的习惯是amend只用于同一个逻辑改动还没推出去的收尾不适合反复拼凑。着色一下场景你写了个登录功能第一次提交漏了判断空密码的逻辑用amend补上这是合理的你提交完登录功能又改了样式再amend进去这就乱了——样式改动应该是另一条独立提交。4.3 stash不想提交又必须切换分支时最后补一个容易被忽略的撤销前先备份思路git stash。它的作用是把工作区所有未提交的改动存到一个独立的堆栈里让工作区回到干净状态然后你随便切分支、拉代码。想恢复的时候再git stash pop。我在两种情况下必用stash一种是改了一半发现需要切出去修个紧急bug另一种是准备执行git reset --hard之前心里没底先把当前改动 stash 住当保险。stash管理有几个常用子命令git stash list查看所有stash记录git stash pop恢复最近一条并从堆栈里移除它git stash apply stash{2}恢复指定某条但不移除。stash的次数多了还可能出现一个坑不同stash里的改动有重叠pop时不认真可能会引入多余的冲突所以用完及时清理。表格式对比一下amend和stash的定位命令使用时机结果git commit --amend最近一条提交需要修补历史被改写不新增提交git stash需要临时保存未提交修改修改被存放可随时恢复git restore丢弃未提交修改修改彻底消失不可恢复5. 常见问题与排查技巧实录5.1 误用hard模式后如何用reflog找回丢失的提交先说最常见的翻车现场。执行了git reset --hard回头发现回退之前的某个提交是需要的这时候别慌git reflog是你的救命稻草。reflog是引用日志记录了所有分支指针和HEAD的每一次移动包括reset、commit、checkout、merge这些操作。执行git reflog会看到一行行类似这样的记录3f9a2c1 HEAD{0}: reset: moving to HEAD~3 e7b8a12 HEAD{1}: commit: 修复登录提示文案 c5d9f31 HEAD{2}: commit: 新增验证码逻辑HEAD{1}就是你在执行reset之前所处的那个提交。要找回它直接git reset --hard e7b8a12这样你的分支就回到误操作之前的那个状态了。如果那个提交的哈希已经完全忘了用网站看了reflog也找不到那就尝试git fsck --lost-foundGit会扫描悬空的提交但能找回来的概率就低很多了。所以我的核心建议仍然不变跑--hard之前先打包分支或打tag真的5秒钟的事能省一整天的抢救时间。5.2 回退后push被拒绝force with lease的正确用法前面反复强调过reset会改写提交历史推送到远程时会跟远程分支产生分歧Git会拒绝推送提示类似hint: Updates were rejected because the tip of your current branch is behind。很多人第一反应是git push -f这就是在团队分支上制造事故的操作方式。如果你确认要走force这条路仅限你个人专属的分支我建议用更安全的变体git push --force-with-lease这个参数的意思是仅当远程分支的状态和我本地记录的一致时才强制推送。换句话说如果你push之前发现远程分支已经被别人推送了新提交force-with-lease会拒绝覆盖保护别人的代码不被你的重置误伤。相比裸的-f这是有原则的强推。在共享分支上永远不要force push到master、develop这类分支这是团队协作的底线。5.3 回退命令选择速查表最后给一张可以贴到显示器边上的决策表。遇到后悔场景时先对着表选命令基本不会出大错场景推荐命令风险等级关键注意点未提交的修改丢弃git restore file低操作不可逆建议先备份未提交的修改留存git stash低记得pop恢复注意stash堆栈顺序暂存区取消暂存git restore --staged file低不会丢工作区改动修改最近一次提交git commit --amend中提交未推送时才能用回退本地最近3次提交git reset --hard HEAD~3高先打tag或记下reflog只撤销提交、保留改动git reset HEAD~1低mixed模式默认回退远程共享分支的提交git revert hash低不能改写共享历史回退合并提交git revert -m 1 hash中先确认父提交方向回退后push被拒git push --force-with-lease高仅在个人分支使用5.4 高频热词延伸提交前最后一道检查聊了这么多回退和撤销说到底都是补救措施。与其事后各种reset、revert、restore来回拉扯不如在提交之前做好最后一道检查。我现在每次提交前都会跑一遍git status git diff --statgit status确认没有混入意外文件git diff --stat扫一眼改动文件数量跟预期是否一致。发现问题就先用git add -p分块暂存把不同逻辑的改动拆开提交。PS关于分支合并、代码库token、ssh认证失败这些高频问题看名字就知道又是另外几篇经典文章的材料——每次在别人电脑上排查ssh认证失败最后十有八九是把钥匙放错了位置或者代理配置没生效跟我们今天聊的回退逻辑一样都是先搞清楚状态再动手的典型场景。我个人最想强调的一点是Git的历史记录是你的财富不是累赘。不要因为嫌提交信息不完美、觉得提交历史太乱就频繁reset改写它。保留完整的历史对后面排查bug、理解业务演进都有巨大的帮助。除非是在自己未推送的本地分支上否则我很少动用reset --hard更多时候用revert和ammend让历史像流水账一样诚实、完整。这个习惯救过我很多次希望也能帮你在时光回溯的路上少踩几个坑。
返回列表