Git 平时用,就那么几个:add、commit、push、pull。但总有那么些时刻 —— 提交错了、文件加错了、改动没了、分支删了、代码"丢了"、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 把改动倒出来接着干。pop 和 apply 的区别: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
思路是"在错的分支上撤销 + 到对的分支上摘过来" —— reset 和 cherry-pick 的组合拳。前提是这次误提交还没 push;如果已经 push 了,main 那边的撤销改用 revert,cherry-pick 到 feature 这步不变。
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 # 结束,回到原来的状态
它会自动在"已知正常"和"已知出错"之间二分:每次检出一个中间版本让你测,你回答 good 或 bad,几轮之后(数学上是 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 到一半,想整个放弃
rebase 或 merge 到一半,冲突一大堆,你解到一半发现思路错了、想从头来过:
git rebase --abort # rebase 到一半冲突,整个放弃、回到操作前 git merge --abort # merge 到一半冲突,整个放弃 git cherry-pick --abort # cherry-pick 冲突了,放弃
--abort 能让你干净地回到操作开始之前的状态,就像这次 rebase/merge/cherry-pick 从没发生过。很多人不知道有这个,冲突解到崩溃了就开始乱删乱改,反而把仓库搞得更乱。记住:解冲突解到反悔了,--abort 是你的"撤销键"。
进阶三:找回误 drop 的 stash
git stash drop 或 git 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 stash或git commit一下。给自己留一个能回退的存档点,永远不亏。从没被 commit 过的改动,神仙也救不回。 - 没 push 的随便折腾,已 push 的用 revert。这条线划清楚,你就永远不会把队友拖下水。
- 记住
git reflog和git fsck的存在。它们是 Git 给你的后悔药 —— 只要东西曾经被 Git 记录过,基本没有真的回不去的状态。
把 Git 的三个区模型理解透,再把这些命令和心法记住,你就从"对 Git 又爱又怕"的状态,升级成了"出了任何状况都知道怎么收拾"的状态。这种踏实感,值得你花这二十分钟。
—— 别看了 · 2026