
1. 为什么“Git 常见场景”值得单独拎出来讲刚入行那会儿我对 Git 的理解长期停留在四个命令add、commit、push、pull。日常写业务代码这套组合拳确实能应付八成情况。直到有一次我在一个多人协作的项目里误把别人的分支合并进了主干又手忙脚乱地强推了一次结果把同事两天的提交全冲掉了。那天下午我们几个人对着终端折腾了三个多小时才把历史找回来从那以后我才真正意识到Git 不是“会用就行”的工具它更像一门需要专门练的手艺平时不显山露水一旦出事就是大事。这篇内容我想聊的就是这件事——Git 常见场景的解决方案。它不是一份命令手册也不是从零开始的教学而是把实际开发中反复出现、又最容易让人卡壳的那些场景一个个拆开讲清楚“为什么会这样”“该怎么处理”“怎么避免下次再踩”。涉及的范围包括但不限于提交历史混乱怎么整理、误操作之后怎么救回来、多人协作时冲突怎么优雅解决、分支模型怎么设计、大文件和历史包袱怎么清理。适合已经能日常提交代码、但一遇到“非标准情况”就发怵的开发者也适合想把自己团队的协作流程理顺的技术负责人。我个人的判断是Git 的难点从来不在命令本身而在于对“提交历史”这个数据结构的理解。你把历史想象成一条由提交节点串成的链每个节点指向它的父节点分支只是一个可移动的指针——只要脑子里有这张图绝大多数“疑难杂症”都能推导出解法。下面我就按场景来展开尽量把每一步背后的逻辑讲透而不是甩给你一串命令让你背。2. 提交历史混乱的整理方案2.1 提交信息写错了、漏了文件怎么补救最常见的场景刚commit完发现提交信息打错字了或者漏加了一个文件。这时候如果还没推送处理起来非常轻松。改最近一次提交的信息用git commit --amend -m 修正后的提交信息如果只是想补充漏掉的文件先git add那个文件再执行git commit --amend --no-edit--no-edit表示沿用原来的提交信息不改。这里的关键点是amend并不是“修改”了那个提交而是用一个新的提交替换掉旧的旧提交会被丢弃。所以只要这个提交还没推送到远端随便改一旦推送了再amend就会造成本地和远端历史不一致后面推送需要强推风险就上来了。提示判断能不能安全amend的标准很简单——这个提交有没有被别人拉取过。只要别人可能已经基于它工作就不要改历史。2.2 多个零碎提交合并成一个开发一个功能时我经常边写边提交最后留下“wip”“fix typo”“再改一下”这种毫无意义的提交记录。交付前把它们整理干净是对 review 的人最基本的尊重。合并最近 N 个提交用交互式变基git rebase -i HEAD~3编辑器会列出最近 3 个提交把想合并的那些行前面的pick改成squash或简写s保存退出后 Git 会让你重新编辑合并后的提交信息。第一个保留pick的提交是“基底”后面的squash都会并入它。这里有个我踩过的坑rebase -i的顺序是从旧到新排列的和git log的显示顺序相反。第一次用的时候我盯着列表愣了半天以为顺序错了。另外如果合并过程中出现冲突解决完冲突后要git add再git rebase --continue千万别直接commit否则会打乱变基流程。2.3 把已经提交的内容拆成多个提交反过来也有需求一个提交里混了两件不相关的事想拆开。做法是软重置到上一个提交git reset --soft HEAD~1--soft的好处是它只移动分支指针工作区和暂存区的内容原封不动所有改动都还在暂存区里。然后你就可以按逻辑分批git add再分别commit。如果想让改动回到工作区而不是暂存区用--mixed默认如果连工作区改动都想丢弃才用--hard——这个参数是危险操作用之前一定确认没有未保存的东西。2.4 整理历史时的注意事项整理历史这件事核心原则只有一条只整理还没共享出去的提交。已经推到远端、别人可能拉过的提交你去改它等于给所有人制造麻烦。团队里如果有人的本地分支基于你改之前的历史他下次拉取就会遇到一堆莫名其妙的冲突。我一般会给自己定个规矩功能分支在提 PR 之前随便整理一旦开了 PR、有人开始 review就冻结历史后续只做增量提交。这样既保证了主干干净又不会给协作者添乱。3. 误操作之后的抢救指南3.1 误删分支、误重置怎么找回来Git 里有个救命命令叫reflog它记录了 HEAD 和分支指针的每一次移动。只要提交对象还在哪怕分支被删了、被reset --hard冲掉了都能找回来。git reflog输出里会看到类似a1b2c3d HEAD{3}: reset: moving to HEAD~2的记录。找到你想回到的那个提交哈希然后git reset --hard a1b2c3d或者更稳妥地新建一个分支指向它git branch rescue-branch a1b2c3d我个人的习惯是后者。直接reset会再次移动当前分支万一又选错了还得再找一次新建分支相当于把现场“冻结”下来慢慢看、慢慢挑心理压力小很多。注意reflog是本地记录默认保留 90 天不可达对象 30 天。如果误操作发生在很久以前或者换过机器reflog可能已经清空了那就只能靠远端仓库或其他备份了。3.2 提交到了错误的分支比如本该提交到feature-a结果手一抖提交到了main。如果还没推送处理起来很干净# 先记下这个提交的哈希 git log --oneline -1 # 切到正确的分支 git checkout feature-a # 把那个提交摘过来 git cherry-pick 哈希 # 回到 main把误提交删掉 git checkout main git reset --hard HEAD~1cherry-pick的作用是“把某个提交的改动复制到当前分支”它生成的是一个全新的提交哈希不同但内容一致。这个命令在跨分支挑拣特定修复时特别有用比如把某个 bugfix 从开发分支同步到发布分支。3.3 强推之后把别人的提交冲掉了这是最惊险的场景也是我开头提到的那次事故。强推push --force会用本地历史覆盖远端如果远端有别人推的提交而你没先拉取那些提交在远端就“消失”了。抢救的关键在于那些提交对象通常还在远端的 reflog 里。如果你有远端机器的访问权限可以上去执行git reflog找到被覆盖前的提交重新建分支推回来。如果没有权限就得联系有权限的同事帮忙。更现实的做法是防患于未然。团队里应该约定主干分支禁止强推需要强推时用--force-with-lease代替--force。--force-with-lease会在推送前检查远端分支是否还是你上次拉取时的状态如果别人推了新东西它会拒绝推送从而避免覆盖别人的工作。这个参数我强烈建议设成肌肉记忆。3.4 误删文件、误改内容想还原单个文件还原到某个提交的版本git checkout 提交哈希 -- path/to/file或者用较新的写法git restore --source提交哈希 path/to/file如果只是想撤销工作区里还没暂存的修改git restore path/to/file就够了。想撤销已经add但还没commit的修改先git restore --staged path/to/file把它移出暂存区再git restore还原工作区。这里容易混淆的是checkout和restore的分工。老版本 Git 里checkout身兼数职切分支、还原文件都靠它容易误操作新版本拆出了switch切分支和restore还原文件语义更清晰。如果你的 Git 版本够新建议养成用switch和restore的习惯。4. 多人协作中的冲突与同步处理4.1 冲突产生的本质与解决流程冲突不是 Git 的 bug而是它“不敢替你决定”的表现。当两个分支修改了同一文件的同一区域Git 无法判断该保留哪个就把决定权交给你。冲突发生时文件里会出现这样的标记 HEAD 当前分支的内容 要合并进来的内容 other-branch解决流程是手动编辑文件删掉这些标记保留你想要的内容然后git add标记为已解决最后git commitmerge 场景或git rebase --continuerebase 场景。我处理冲突时有个习惯先看两边改动的意图而不是急着选一边。很多时候冲突的根源是两个人对同一段逻辑做了不同假设这时候最好的做法不是二选一而是把两边的意图融合成一个更完整的实现。机械地“保留我的”或“保留他的”往往埋下隐患。4.2 merge 和 rebase 到底怎么选这是被问得最多的问题之一。简单说维度mergerebase历史形态保留分叉有合并节点线性无合并节点提交哈希不变全部重写适用场景公共分支、已共享分支个人分支、整理后再合并冲突处理一次性解决可能每个提交都要解决我的实践原则是个人功能分支用 rebase 保持线性合并回主干时用 merge或 squash merge保留合并记录。这样既让主干历史清晰又不会重写已共享的提交。需要特别提醒的是永远不要对已经推送到公共分支的提交做 rebase。因为 rebase 会重写哈希别人基于旧哈希的工作全部对不上协作会瞬间变成灾难。4.3 拉取时用 pull 还是 fetch mergegit pull本质上是fetchmerge或fetchrebase取决于配置。我个人的习惯是先 fetch 再手动决定git fetch origin git log HEAD..origin/main --oneline # 看看远端多了什么 git merge origin/main # 或 git rebase origin/main这样做的好处是在真正合并之前你能先看清楚远端带来了哪些改动心里有数再动手。直接pull有时候会在你毫无准备的情况下触发一堆冲突尤其是在你本地有未提交改动的时候。可以配置git config --global pull.rebase true让pull默认走 rebase保持历史线性。但这个配置因人而异团队里最好统一否则有人 merge 有人 rebase历史会变得很乱。4.4 协作中的分支模型建议小团队3 到 5 人我推荐简化版的分支模型main保持随时可发布功能开发开feature/*分支修紧急 bug 开hotfix/*分支。功能分支从main切出完成后通过 PR 合并回main。分支命名上我建议带上用途和负责人比如feature/user-login、hotfix/payment-timeout。别用dev1、test这种名字过两天自己都忘了是干嘛的。还有一个容易被忽视的点功能分支要短命。一个分支活过一周合并时的冲突量会指数级上升。我见过有人一个分支开了两个月最后合并时冲突多到直接放弃重写。所以功能拆小、频繁合并是减少协作痛苦的根本办法。5. 仓库体积与历史包袱的清理5.1 大文件是怎么把仓库撑爆的Git 会保存每一次提交的完整快照通过对象存储和压缩所以如果你曾经提交过一个几百 MB 的二进制文件哪怕后来删掉了它依然躺在历史里每次 clone 都要把它拉下来。时间一长仓库体积可能从几十 MB 膨胀到几个 GB。判断仓库里哪些文件占了大头可以用git rev-list --objects --all | git cat-file --batch-check%(objecttype) %(objectname) %(objectsize) %(rest) | sort -k3 -n -r | head -20这条命令会列出历史中体积最大的 20 个对象。看到结果你往往会惊讶原来是一个早就删掉的视频文件或者依赖包在作祟。5.2 用 filter-repo 清理历史老牌的filter-branch又慢又容易出错现在推荐用git filter-repo。清理某个大文件git filter-repo --path path/to/big-file --invert-paths--invert-paths表示“排除这个路径”也就是把它从所有历史中删掉。执行完这个命令所有提交的哈希都会变所以必须在所有人都知情的情况下操作并且操作后所有人重新 clone。注意filter-repo不是 Git 自带的需要单独安装。操作前务必备份整个仓库目录因为一旦执行原始历史就没了。5.3 预防胜于治疗gitignore 和钩子清理历史是“手术”预防才是“养生”。项目一开始就要写好.gitignore把编译产物、依赖目录、日志、本地配置都排除掉。常见的坑是先提交了node_modules后来才想起来加.gitignore但历史里已经有了得走上面的清理流程。可以配合 Git 钩子在提交前做检查。比如在.git/hooks/pre-commit里加一段脚本检测暂存区里有没有超过某个大小的文件有就拒绝提交。这样能在源头拦住大文件。我个人的经验是任何超过 1 MB 的二进制文件提交前都要想一想是不是真的该进 Git。图片、字体、数据集这类东西更适合放在专门的资源管理方案里而不是塞进代码仓库。6. 常见问题速查与避坑心得6.1 高频问题速查表场景命令关键点改最近提交信息git commit --amend仅限未推送合并多个提交git rebase -i HEAD~N顺序从旧到新拆分一个提交git reset --soft HEAD~1改动保留在暂存区找回误删分支git refloggit branch依赖本地记录跨分支摘提交git cherry-pick hash生成新哈希安全强推git push --force-with-lease防止覆盖他人还原单文件git restore --sourcehash file语义清晰清理大文件git filter-repo --path X --invert-paths会重写全部历史6.2 我踩过的几个坑第一个坑是在 rebase 过程中用 commit 代替 continue。有次解决完冲突我下意识地git commit结果 Git 把这次提交当成了新的独立提交整个变基流程乱掉了。正确做法永远是git add之后git rebase --continue。第二个坑是以为.gitignore对已跟踪文件生效。.gitignore只对未跟踪的文件起作用已经提交过的文件即使写进.gitignore也照样被跟踪。要让它失效得先git rm --cached把它从索引里移除。第三个坑是在错误的目录执行 Git 命令。Git 会向上查找.git目录所以你在子目录里执行命令操作的其实是父目录的仓库。有次我在一个嵌套的项目里reset --hard以为只影响当前子项目结果把整个大仓库都重置了。养成git status先看一眼当前分支和仓库位置的习惯能省很多事。6.3 给不同阶段开发者的建议如果你刚工作不久我的建议是先把 reflog 和 amend 这两个工具用熟。它们能覆盖你 80% 的“手滑”场景而且风险低。等你对提交历史的结构有了直觉再去碰 rebase 和 filter-repo 这类会重写历史的操作。如果你已经在带团队那重点应该放在流程约定上主干保护、分支命名规范、PR 前整理历史、禁止对公共分支强推。工具层面的问题好解决流程层面的混乱才是真正拖慢团队的东西。最后分享一个我自己的小习惯每次执行可能“危险”的命令reset --hard、push --force、filter-repo之前先git branch backup-$(date %s)建一个临时备份分支。这个动作只花一秒钟但真出事的时候它能让你从“完了”变成“还好”。这个习惯我坚持了好几年救过我至少三次。