Git 救命指南:12 个迟早会用到的命令(改错 / 误删 / 丢代码全场景修复)

Git 平时用,就那么几个:addcommitpushpull。但总有那么些时刻 —— 提交错了、文件加错了、改动没了、分支删了、代码"丢了"、rebase 搞砸了 —— 你会突然需要一些平时根本不碰的命令。而那个时刻,你通常正手忙脚乱、心跳加速,没有心情现搜文档、对着搜索结果一个个试。

这篇把那些"迟早会用到"的救命操作系统地整理出来 —— 先讲清楚一个让所有命令都变简单的底层模型,然后是十几个高频场景 + 进阶技巧,每个都给命令、讲清楚原理、标好坑。建议收藏,真出事的时候照着做就行。文章偏长,但 Git 这东西,理解了底层模型,记起来其实不费劲。

先理解一个模型:Git 的三个区

几乎所有的 Git "补救"操作,本质都是一件事:把某个东西,在几个区之间挪一挪。所以先把这几个区搞清楚,后面的命令就不是"死记",而是"能推导"了。

Git 的三个区(理解这个,后面所有命令都顺了):

  工作区          暂存区(Index)        版本库(仓库)
  你在编辑的文件 ──git add──► 准备提交的快照 ──git commit──► 永久历史
        ▲                  ▲                      │
        └── git restore ───┘                      │
        └──────── git reset 把指针往回拨 ◄─────────┘

  · 大多数"补救"操作,本质都是"把某个东西在这三个区之间挪一挪"
  · 还有一个隐藏的"第四区":stash(储藏室),临时存放工作区改动

三个核心区:工作区(你正在编辑的文件)、暂存区(你 git add 之后、准备一起提交的那个快照)、版本库(git commit 之后形成的永久历史)。再加一个隐藏的"储藏室"stash

带着这个模型看后面的命令:git reset 是"把版本库的指针往回拨,改动落到哪个区由参数决定";git restore 是"在工作区/暂存区之间挪或丢弃";git stash 是"把工作区改动临时收进储藏室";git reflog 是"版本库指针移动的历史记录"。理解了"东西在区之间流动",你就不会再死记硬背了。

1. 改错了最后一次提交

提交信息打错字了,或者提交完才发现漏了个文件。只要这次提交还没 push,都能干净地补救:

git commit --amend                 # 打开编辑器,改最后一次提交的信息

如果只是想把漏掉的文件补进去、提交信息不用动:

git add 漏掉的文件
git commit --amend --no-edit       # 把文件补进上次提交,提交信息不变

坑:--amend 的本质是"用一次新提交,替换掉最后一次提交" —— 它生成的是一个全新的 commit(hash 都变了),不是"修改"原来那个。所以,已经 push 到共享分支的提交别 amend:你本地的和远程对不上了,再推就得强推,会影响所有同步过这个分支的人。--amend 只对"还没推出去的"提交是安全的。

2. 撤销提交,但改动要保留

不小心把东西 commit 了,想把"提交"这个动作撤销,但代码改动一点都不能丢。这是 git reset 的主场,关键在于选对参数:

git reset --soft  HEAD~1           # 撤销提交,改动留在【暂存区】,可直接重新提交
git reset --mixed HEAD~1           # 撤销提交,改动退回【工作区】(--mixed 是默认值)
git reset HEAD <file>              # 把某个文件移出暂存区(改动还在工作区)

对照三个区的模型就很清楚了:reset 把版本库的指针往回拨一格,被"拨掉"的那次提交里的改动去哪了取决于参数 —— --soft 让改动留在暂存区(可直接重新 commit),--mixed(默认)让改动退回工作区(要重新 add 再 commit)。两者的共同点:改动都还在,只是所在的区不同。记住:"soft 退到暂存区,mixed 退到工作区,代码都没丢"。

3. 彻底丢弃提交和它的改动

这次提交、以及它带来的改动,你都确定不要了:

git reset --hard HEAD~1            # 撤销提交 + 丢弃所有改动(危险!但 commit 过的仍能靠 reflog 救)

--hard 会把提交和工作区改动一起抹掉 —— 这是 reset 三个参数里唯一会真的丢代码的,也是整篇里最危险的命令之一。不过别太怕:即使误用了,只要被抹掉的内容曾经被 commit 过,第 5 条的 reflog 还能捞回来。真正捞不回的,是从来没被 commit 过的改动 —— 所以才有那条铁律:做危险操作前先 commit 或 stash。

4. 丢弃工作区的改动,回到没动过的样子

git restore <file>                # 丢弃工作区某个文件的改动
git restore .                     # 丢弃所有未暂存的改动(危险,执行前想清楚)
git restore --staged <file>       # 把文件从暂存区拿回工作区(= git reset HEAD <file>)

git restore . 会把所有未暂存的改动一笔勾销,而且不进任何记录、不可恢复。拿不准的时候,先 git stash(见第 6 条)留个后路。git restore 是较新的命令,作用更聚焦;老命令 git checkout -- <file> 效果一样,但 checkout 一词多义、容易误用,新代码建议用 restore

5. 代码"丢了" —— 真正的救命稻草

这是整篇里最值得记死的一条:只要你 commit 过,在 Git 里就几乎没有真正丢失的代码。

reset --hard 了、误删了分支、rebase 搞砸了、amend 把东西覆盖了…… 只要那些提交曾经存在过,git reflog 都能帮你找回来。它记录了你本地 HEAD 指针的每一次移动 —— 每次 commit、reset、checkout、rebase,都留了一条带 hash 的记录。

git reflog                        # 列出 HEAD 的每一次移动,每条带一个 hash
git reset --hard <出事前那条记录的hash>   # 时光机:回到任意一个历史状态

用法:在 reflog 列表里找到"出事之前"的那个状态,把它的 hash 拿去 git reset --hard,就回去了。很多人用 Git 好几年都不知道 reflog 的存在,直到第一次靠它捞回半天的工作量 —— 然后这辈子都忘不掉。记住有 reflog 这个东西,本身就能在关键时刻救你一命。(注意:reflog 是本地的、有时效的,默认大概保留 90 天,出事要尽快用。)

6. 临时存放改动,干净地切分支

正在 A 分支改东西改到一半,突然要切去 B 分支处理急事,但手里的改动还没到能提交的程度:

git stash                         # 把当前改动打包收起,工作区瞬间变干净
git stash pop                     # 把最近一次 stash 的改动倒出来,接着干
git stash list                    # 查看 stash 了哪些
git stash apply stash@{1}         # 取出指定的某次 stash(apply 不删除,pop 会删除)
git stash -u                      # 连未追踪的新文件一起 stash

git stash 把当前改动打包收起,工作区瞬间变干净,你可以随便切分支。事情处理完切回来,git stash pop 把改动倒出来接着干。popapply 的区别:pop 取出后会把这条 stash 删掉,apply 取出后保留 —— 要把同一份 stash 应用到多个分支就用 apply。另外默认的 git stash 不会收"未追踪的新文件",要带 -u

7. 只想要别人分支上的某一个提交

另一个分支上有一次提交,你只想要那一个 commit,不想把整条分支合过来:

git cherry-pick <commit-hash>     # 把指定的某次提交「摘」到当前分支
git cherry-pick A^..B             # 摘取 A(不含)到 B 之间的一串提交

cherry-pick 会把指定的那次提交"摘"到你当前分支上。典型用途:修了个 bug 想同步到多个维护分支、把某个功能从实验分支单独捞出来、把误提交到错分支的提交搬回去。它也能一次摘一串连续的提交。

8. 撤销一个已经 push 出去的提交

提交已经推到共享分支了,这时候千万别用 reset —— reset 会改写历史,逼着所有人强行同步,是团队协作里的灾难。正确做法是用 revert:

git revert <commit-hash>          # 生成一个「反向提交」来抵消它,历史不被改写,可安全推送
git revert -m 1 <merge-commit>    # 撤销一个 merge 提交(-m 1 指明保留主线那一支)

revert 不删除任何历史,而是新增一个"反向提交"来抵消目标提交的改动。历史一直往前走,所有人正常 pull 就行,不会有冲突灾难。铁律:没 push 的用 reset,已 push 的用 revert。撤销 merge 提交要多带 -m 参数指明保留哪条主线(因为 merge 提交有两个父节点)。

9. 提交到错的分支上了

本该提到 feature,结果手一抖提到了 main。把它搬回去:

# 在 main 上误提交了,本该提到 feature
git log --oneline -1              # 记下这次提交的 hash
git reset --hard HEAD~1           # main 退回去
git checkout feature
git cherry-pick <刚才记下的hash>   # 把提交搬到 feature

思路是"在错的分支上撤销 + 到对的分支上摘过来" —— resetcherry-pick 的组合拳。前提是这次误提交还没 push;如果已经 push 了,main 那边的撤销改用 revert,cherry-pickfeature 这步不变。

10. 误删了分支

手滑 git branch -D 把一个还有用的分支删了。别慌 —— 分支只是一个指向某次提交的指针,删掉指针,提交本身还在。

git reflog                        # 找到被删分支最后那次提交的 hash
git branch <分支名> <那个hash>     # 用 hash 把分支重新建出来

又是 reflog 救场。它能列出那个分支最后指向的提交 hash,你拿这个 hash 重新 git branch 一下,分支就回来了,上面的提交一个不少。

11. 已经提交的文件,想加进 .gitignore

很常见的失误:某个本地配置、日志文件、node_modules.env 已经被 commit 进仓库了,这时候光往 .gitignore 加一行是没用的 —— Git 对已追踪的文件根本不看 .gitignore。得先让 Git "忘掉"它:

echo "config.local.js" >> .gitignore
git rm --cached config.local.js   # 从 Git 追踪里移除,但【保留】本地文件
git rm -r --cached node_modules   # 目录同理,加 -r
git commit -m "chore: stop tracking config.local.js"

--cached 是关键:它只把文件从 Git 的追踪里移除,本地文件本身不删。提交之后,这个文件就既留在你本地、又不再被 Git 管了。(如果是 .env 这种敏感文件已经被提交过,光这样还不够 —— 它在历史里还能被翻出来,真涉及密钥泄露还得改密钥、必要时清理整段历史。)

12. 用二分法揪出引入 bug 的提交

"上周还好好的,现在坏了,中间几十个提交,到底是哪个搞坏的?"—— 靠肉眼一个个 checkout 去试,能试到天黑。git bisect 用二分查找帮你定位:

git bisect start
git bisect bad                    # 当前版本是坏的
git bisect good <一个已知正常的旧commit>
# Git 自动二分检出,你测一次答一次 git bisect good / git bisect bad
# 几轮后它会告诉你:是哪个 commit 第一个引入了 bug
git bisect run ./test.sh          # 有测试脚本时,让它全自动跑完
git bisect reset                  # 结束,回到原来的状态

它会自动在"已知正常"和"已知出错"之间二分:每次检出一个中间版本让你测,你回答 goodbad,几轮之后(数学上是 log₂ 次)就能精确指出"是哪个 commit 第一个引入了问题"。几十个提交,通常五六次就定位到了。如果你有自动化测试脚本,用 git bisect run 还能让它全自动跑完,堪称神器。

进阶一:用交互式 rebase 整理提交历史

开发过程中难免留下一堆"修个错别字""再改改""哦又漏了"这种零碎提交。push 之前用交互式 rebase 把它们整理干净,提交历史会清爽很多,也方便别人 review:

git rebase -i HEAD~3              # 交互式整理最近 3 次提交
# 编辑器里把某行的 pick 改成:
#   squash / s  —— 把这条合并到上一条
#   reword / r  —— 只改这条的提交信息
#   drop   / d  —— 删掉这次提交
#   edit   / e  —— 停在这里,让你修改
#   直接调整行的顺序 = 调整提交顺序

交互式 rebase 能合并(squash)、改信息(reword)、删除(drop)、停下来修改(edit)、甚至重排提交顺序。铁律:它会改写历史,只对"还没 push"的提交用。整理自己本地的提交非常好用,但绝不要拿去动已经共享的分支。

进阶二:rebase / merge 到一半,想整个放弃

rebasemerge 到一半,冲突一大堆,你解到一半发现思路错了、想从头来过:

git rebase --abort                # rebase 到一半冲突,整个放弃、回到操作前
git merge  --abort                # merge 到一半冲突,整个放弃
git cherry-pick --abort           # cherry-pick 冲突了,放弃

--abort 能让你干净地回到操作开始之前的状态,就像这次 rebase/merge/cherry-pick 从没发生过。很多人不知道有这个,冲突解到崩溃了就开始乱删乱改,反而把仓库搞得更乱。记住:解冲突解到反悔了,--abort 是你的"撤销键"。

进阶三:找回误 drop 的 stash

git stash dropgit stash clear 手滑了,把还有用的 stash 删了。stash 不在普通的 reflog 里,但它对应的 commit 对象还在 Git 仓库里"游离"着,没被立刻回收:

git stash list                    # 先看 stash 列表(误 drop 后这里没了)
git fsck --no-reflog | grep commit  # 列出"游离"的提交对象
# 在结果里找到那个 stash 对应的 commit hash,然后:
git stash apply <那个hash>         # 把它救回来

git fsck --no-reflog 能列出所有"游离"的提交对象 —— 误删的 stash 就在里面。找到对应的 hash,git stash apply <hash> 就能救回来。这是个冷门但关键时刻能救命的技巧。

进阶四:git worktree —— 不用切分支,同时干两件事

经典痛点:正在 feature 分支写到一半,线上来了个紧急 bug 要修。常规做法是 stash → 切 hotfix → 修 → 切回 → stash pop,一通切来切去很烦,还容易乱。git worktree 提供了另一种思路:

git worktree add ../hotfix-dir hotfix   # 把 hotfix 分支检出到另一个目录
# 现在你有两个目录:当前目录还在 feature,../hotfix-dir 在 hotfix
# 改完 hotfix,git worktree remove ../hotfix-dir
# 好处:不用 stash、不用切分支,两个分支同时"摊开"在两个目录里干活

git worktree 能把不同的分支同时检出到不同的目录。你的当前目录还在 feature、原封不动,另一个目录是 hotfix,两边可以同时干活、互不干扰。修完 hotfix,把那个 worktree 目录移除即可。对于"经常要并行处理多个分支"的人,这个命令能大幅减少切换的心智负担。

FAQ

reset 和 revert 到底怎么选?一句话:看提交有没有 push。没 push、只在本地的,随便 reset;已经 push 到共享分支的,一律用 revert。混了的话,你会把队友拖进"为什么我 pull 下来一堆冲突"的泥潭。

reflog 会一直保留所有记录吗?不会。reflog 是本地的,且有过期清理机制(默认大概 90 天)。它是"近期的后悔药",不是"永久存档",出事了要尽快用。

reset / revert / restore 老是搞混?用三个区的模型记:reset 动的是"版本库指针"(往回拨);revert 动的是"往前加一个反向提交";restore 动的是"工作区/暂存区里的文件"。三个层面,各管各的。

解决冲突有什么技巧?核心是看懂冲突标记:<<<<<<<======= 之间是一边,=======>>>>>>> 之间是另一边。你要做的不是"二选一",而是理解两边各自想做什么,然后写出一个都满足的最终版本,再把标记行删掉。解不下去就 --abort 重来,别硬扛。

怎么写好提交信息?推荐 conventional commits 风格:类型: 简短描述,类型常用 feat(新功能)、fix(修 bug)、refactor(重构)、docs(文档)、chore(杂项)。一次提交只做一件事 —— 这样上面那些"撤销/摘取/二分"的命令才好用,一个提交里塞了五件事,你想单独撤其中一件就难了。

git pull 时总是产生一个莫名其妙的 merge 提交,怎么办?那是 pull 默认会 merge。可以配置 git config --global pull.rebase true,让 pull 改用 rebase,历史会更线性、干净。

写在最后:三条万能心法

这些命令的共同点是:平时用不上,用上时往往在救火。值得花十分钟把它们各跑一遍、心里有个底 —— 真出事的时候,"知道有办法"和"完全懵了"是两种完全不同的体验。

最后给三条贯穿全篇的心法:

  • 动手补救前,先 git stashgit commit 一下。给自己留一个能回退的存档点,永远不亏。从没被 commit 过的改动,神仙也救不回。
  • 没 push 的随便折腾,已 push 的用 revert。这条线划清楚,你就永远不会把队友拖下水。
  • 记住 git refloggit fsck 的存在。它们是 Git 给你的后悔药 —— 只要东西曾经被 Git 记录过,基本没有真的回不去的状态。

把 Git 的三个区模型理解透,再把这些命令和心法记住,你就从"对 Git 又爱又怕"的状态,升级成了"出了任何状况都知道怎么收拾"的状态。这种踏实感,值得你花这二十分钟。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

JavaScript 防抖与节流彻底搞懂:原理、区别、手写实现 + 实战避坑指南

2026-5-14 16:04:43

技术教程

Flexbox 还是 Grid?2026 前端布局终极选择指南(附决策清单)

2026-5-14 16:32:30

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索