Git 这玩意儿说难不难说简单也真不简单。日常开发里我见过太多人把一整套 Git 工作流用得飞起却在某次 rebase 冲突、一次误删分支、或者一个突如其来的“快进失败”面前彻底卡住。我自己也踩过不少坑有些问题甚至反复出现每次都要重新翻文档、查命令。后来我索性开始维护一份自己的 Git 使用问题记录把每一条症状、原因、解决过程、事后心得都写下来坚持了一段时间之后Git 相关的“意外事故”在我手里基本就绝迹了。这份记录不是教科书式的命令大全而是真正从失败现场里沉淀出来的东西。今天整理出来分享给大家内容偏向实际开发中的高频场景比如分支合并、撤销回滚、远程同步、子模块、大仓库处理这些适合所有被 Git 卡住过的开发者。不管你是刚接手团队项目的新人还是用了好几年 Git 但偶尔还要搜命令的老手我猜你多少都能在这份记录里找到自己曾经遇到的问题。1. 我为什么要专门维护一份 Git 问题记录1.1 先聊聊这份记录的由来最早的时候Git 问题对我而言就是“报错就搜”。可问题在于搜出来的答案往往是零散的有些命令还跟自己的 Git 版本对不上更致命的是很多方案只是解决了表面报错没有解释底层原因。于是同样的坑换个场景又踩一次比如“把 rebase 当成 merge 用”“reset 之后发现提交推不上去”“submodule 更新到一半导致仓库状态撕裂”……这类问题出现的频率远比想象中高。后来我换了思路每次遇到问题除了解决它还会花五分钟把以下内容记下来问题出现的操作路径、完整报错信息、我当时查到的原因解释、实际使用过的解决命令、以及这个命令为什么有效。记录多了以后我发现一个很明显的规律——大部分人的 Git 痛苦都是相似的绕来绕去就那么十几个经典场景。1.2 这份记录的核心价值不在于“记”而在于“归类”光记录不整理那叫流水账。我的做法是定期把记录按照“操作类型”归类每一类下面再按“症状”拆开。比如“分支信息不同步”“merge 卡住”“rebase 后状态错乱”全都归到“分支与合并”这一类底下。这样当问题再次出现时我不是在搜索答案而是在自己的知识库里检索同类型问题的处理套路速度快得多。归类之后还会发现另一件有意思的事很多表面不同的报错本质上都是同一个底层机制导致的。比如“merge 冲突”和“stash pop 冲突”看起来八竿子打不着其实都是因为 Git 需要把两个分支的改动合并到同一个工作区文件里。理解了这一点你就不会慌因为你知道冲突的本质是什么也知道 Git 给你留了哪些后路。2. 分支与合并高频问题集中营2.1 merge 执行到一半卡住了怎么安全脱身这是典型的“做到一半发现做错了”的场景。比如你把某个功能分支合并到主分支执行完 merge 命令后 Git 提示有冲突一堆文件变成 Unmerged 状态。这时候你发现其实根本不应该合并或者合并的时机不对想退出这个状态。很多人在这个时候第一反应是直接git merge --abort但要注意一个细节如果你是从别的分支切过来执行 mergeabort 会把你带回 merge 开始前的状态理论上没毛病。但如果 merge 命令已经提交完成比如没有冲突或者你已经手动解决了冲突并提交了abort 就救不了你了这时候需要用git reset --hard回到 merge 之前的提交。还有个更隐蔽的情况合并过程中你不仅处理了冲突还顺手改了别的文件。abort 会把所有改动都还原成 merge 之前的样子你自己改的那些文件也会被一并清理掉。所以我在记录里给自己定了一条规矩——执行任何可能回退仓库状态的命令之前先确认工作区里没有想保留又没提交的东西。真有不放心的改动先 stash 或者 commit 到临时分支上再处理合并问题。2.2 rebase 冲突和 merge 冲突解决思路完全不同很多人对 rebase 有恐惧感我觉得很大原因是没搞清楚 rebase 的本质。merge 是“把两条路汇到一起”rebase 是“把你当前分支上的提交一个个摘下来重新放到目标分支的最新提交后面”。所以 rebase 冲突不是一次性解决完就结束的而是每应用一个提交就可能触发一次冲突。换句话说rebase 冲突可能发生在多个连续的节点上你得反复解决、反复 continue。举个例子你的功能分支基于主分支的某个旧提交开发提交了三个 commit主分支后来也有更新。这时候对功能分支执行 rebase 主分支Git 会先尝试把第一个 commit 应用上去如果这部分代码和主分支的最新改动冲突你就得先解决这一处冲突git add之后执行git rebase --continue然后 Git 再尝试应用第二个 commit。这跟 merge 一次性处理所有冲突的体验是完全不一样的。实际操作中我更喜欢在 rebase 前先用git merge-base查看两个分支的共同祖先评估一下 commit 数量如果功能分支上提交很多、共同祖先离现在又很远我会认真考虑是不是应该改用 merge 而不是 rebase。记住一个原则rebase 会让提交历史更线性但它是有代价的代价就是你可能要面对多轮冲突。2.3 冲突标记怎么读以及让冲突更可读的三个配置Git 冲突标记长这样 HEAD 这里是当前分支的内容 这里是正在合并进来的内容 feature-branch这个结构本身不难理解真正的难点在于当一个文件里有连续大段冲突时你很难判断每一段对应的是哪个逻辑区域。我记录下来的实用技巧是先不要急着改代码用git diff把冲突文件整体看一遍分清哪些块是同一处逻辑的重复哪些块是真要合并的。然后再动手很多东西并到一起之后你发现真正需要人工取舍的其实只占一小部分。另外有两个配置我觉得非常值得开merge.conflictStyle设置为diff3这样冲突区域会额外显示一个共同祖先版本有了它你能更清楚地知道双方的改动分别基于什么状态判断起来轻松很多。还有一个是merge.keepBackup建议保持默认因为解决冲突后 Git 生成的.orig备份文件虽然碍眼但关键时刻能救命。3. 撤销与回滚那些后悔药要吃对型号3.1 reset 的三种模式以及 soft 到底何时用git reset大概是所有人最早学会的撤销命令但它包含的信息量比很多人以为的大得多。reset 有三个模式--soft、--mixed默认、--hard区别在于它会移动 HEAD 指针、重置暂存区、重置工作区这三个动作各做多少。我用一句话总结就是reset --soft只把 HEAD 指针移回去代码改动还在暂存区reset --mixed额外把暂存区也还原代码改动留在工作区reset --hard连工作区的文件内容都还原改动直接消失。日常最常遇到的问题是有人提交错了想撤销用了git reset --hard HEAD~1然后发现文件里自己还没提交的其他改动也被清掉了当场崩溃。所以我在自己的记录里写了一条非常明确的建议默认情况下优先使用git reset --mixed HEAD~1或者git reset --soft HEAD~1除非你真的确定工作区里没有任何需要保留的东西。--hard是最后的选择而且在用之前最好先git stash或者备份一下当前工作区的状态。3.2 提交已经推到了远端还能不能用 reset 撤销这个问题困扰过很多人。先说结论提交已经推到远端如果那个分支只有你自己在用你仍然可以 reset然后强行 push 覆盖。但如果那个分支还有别人在协作reset 会导致别人的本地历史和你推上去的新历史不一致他们下一次 pull 就会遇到一堆匪夷所思的冲突甚至直接报错。更稳妥、也更符合团队协作习惯的做法是使用git revert。revert 不会删除历史提交而是生成一个新的提交把目标提交的改动反过来应用回去。这样远端历史会一直保持线性增长其他人 pull 的时候不会有任何障碍。它的缺陷在于提交历史会多出一条“反做”记录看起来不够干净但对多人协作来说干净永远没有安全重要。3.3 误删分支或者错误提交之后怎么用 reflog 找回这是我最想让大家记住的救命技能。只要你在本地执行过提交哪怕后来被 reset、被删除、被 revert提交对象通常都还残留在 Git 的对象库里只是没有任何引用指向它而已。这时候git reflog就是唯一的救命线索。reflog 记录的是你本地 HEAD 指针的每一次移动轨迹。比如你执行了git reset --hard 某个旧提交reflog 里就会留下移动前的提交哈希。猛然发现搞错了立刻执行git reflog找到 reset 之前的那条记录复制对应的哈希然后git reset --hard 那个哈希就能原样找回。如果是删除了整个分支也一样先用git reflog或者git fsck --lost-found找到分支头部的提交哈希再基于这个哈希重新创建分支。我还会额外做一件事定期给重要的里程碑提交打 tag因为 tag 是稳定的引用不会像分支一样因为误删而消失。4. 远程仓库的疑难杂症同步、凭证与子模块4.1 推送被拒绝远程有本地没有的提交“! [rejected] ... (fetch first)”大概是新手遇到最多、也最容易慌的报错。很多人直接照着提示执行git pull结果 pull 的时候又碰到了合并冲突整个推到流程变得非常混乱。我的套路是先冷静分析再动手。首先执行git fetch origin把远程的最新状态拉到本地但不合并然后执行git log origin/当前分支名 --oneline看看远程多了哪些提交。接着执行git merge 或 git rebase我的偏好是如果改动涉及同一个文件的相同区域用 rebase 会把本地提交移到远程提交之后冲突处理起来更线性如果涉及不同文件merge 也一样安全。处理完冲突再重新 push。还有一类推送失败是因为本地分支和远程分支的关联关系丢了提示“git push --set-upstream”。这种情况通常发生在你手动删除过远程分支或者本地分支是从另一个分支切出来而不是从远程拉出来的。解决办法不复杂git branch --set-upstream-toorigin/分支名 本地分支名就能重新建立关联。4.2 换机器之后凭证失效频繁要求输入账号密码这个问题实际开发里非常常见。你在自己电脑上配置好了凭证存储换一台机器或者把公司电脑带回家发现每次和远程仓库交互都被要求重新输账号密码项目一多简直痛不欲生。解决思路取决于你的远程仓库使用的是什么协议。如果你用的是 HTTPS 方式克隆的仓库Git 默认的凭证过期策略会让你反复输入认证信息。可以调整凭证存储方式在 Git 的全局配置里设置credential.helper比如在 Windows 上设置为managermacOS 上设置为osxkeychain这样第一次认证成功后凭证会被安全保存后续操作就不用重复输入。如果你用的是基于 SSH 的协议那问题的关键就不是凭证存储而是你本机的密钥有没有添加到远程仓库账户里以及密钥的路径是否在加载列表中。4.3 submodule 更新不回来切了分支但内容还是老版本submodule 问题在多人协作的项目里相当磨人。经常出现的情况是主仓库执行了git pull看到某个子模块的引用变了但进入子模块目录一看代码还是旧版本。你去执行git submodule update它又说没有更新任何内容因为子模块当前所在的提交可能并不匹配主仓库记录的指针。这时候最直接的方法是根据主仓库记录的子模块提交哈希强制检出对应提交。还有一种常见场景是子模块的远程分支变了旧分支被删除你需要在子模块目录里手动修改 remote 地址或者重新拉取。我的经验是submodule 更新之前先确认三件事主仓库是否已经 pull、子模块是否处于 detached HEAD 状态、子模块的 remote 是否还能访问。这三件事都对了git submodule update --remote通常就能把子模块拉到正确版本。5. 那些不起眼却特别耗时的日常小坑5.1 stash pop 和 stash apply以及弹错分支的救援git stash是保存工作区改动的好工具但它在某些情况下也会咬人。最常见的问题你在 A 分支 stash 了一批改动切到 B 分支想用git stash pop弹回来结果 B 分支上已经有人改了同样的代码Git 弹出了冲突。这时候不像 merge 冲突stash pop 之后原来的 stash 记录可能已经被清掉了看起来好像改动丢了。实际上 stash 记录并不会这么容易消失。如果你用的是git stash pop且发生了冲突原来那条 stash 记录还会保留直到你完全解决冲突并再次尝试 pop。如果你用的是git stash apply它永远不会删除 stash 记录所以会更安全。我被坑过几次之后开始养成了一个习惯跨分支使用 stash 时先git stash show -p stash{0} | git apply --reverse做一次演练确认能顺利反向应用再决定是否真的 pop避免把改动带入错误的分支。5.2 worktree终于可以同时开两个分支互不干扰这个功能我用了很久才真正接纳它。以前要在同一台机器上同时处理两个分支的代码我都是建两个仓库目录每个目录克隆一份代码。后来发现 Git 自带的git worktree可以让我在一个目录下挂载多个工作树每个工作树对应不同的分支且彼此完全独立。高频用法是当你在主要工作目录里改到一半突然要紧急修复另一个分支的问题不需要 stash 或者 commit直接git worktree add ../项目名-hotfix 修复分支名然后在那个目录里专注修复。修完再回到主目录继续原来的工作互相零干扰。这个功能在配合 IDE 的工作区配置时尤其好用不用反复切换窗口和分支。清理时用git worktree remove就行不会影响到主仓库里的分支数据。5.3 reflog 的日常用法而不仅仅是救火reflog 最大的价值虽然体现在找回误删提交上但日常开发里它也是一个极好的“日志”。比如你忘了自己上次在这个仓库里做了什么或者你想确认某个分支最近几天有没有被动过git reflog --dateiso就能看到所有 HEAD 移动事件和对应的操作时间。我自己的习惯是每周抽五分钟git reflog过一遍所有分支的移动记录同时顺手删掉一些已经合并进来的老旧分支清理一下本地仓库的状态。这些不起眼的小习惯看起来没什么技术含量但它们帮你把混乱拦在了事故发生之前。6. 大仓库与历史清理一次不太愉快但很有收获的实战6.1 仓库越来越臃肿clone 一次要等半天团队项目用着用着仓库体积会膨胀到让人怀疑人生。最常见的原因就是有人在仓库里提交了体积较大的二进制文件或者把依赖包、构建产物直接塞进了版本库。虽然 Git 对文本压缩很有效但二进制文件无论怎么压缩体积都摆在那里而且每一个历史版本都会保留一份快照仓库因此只增不减。排查方法很简单用git rev-list --objects --all配合git cat-file --batch-check找出体积排名靠前的对象。拿到对象列表后根据文件名基本就能判断是哪个目录产生的垃圾文件。处理方式分两种如果团队以后还可能需要这些文件的旧版本可以考虑引入 Git LFS 来管理把大文件指针提交到仓库、真实内容放到 LFS 存储如果这些文件根本不该入库那就要做历史改写把它彻底剔除。6.2 历史改写如何在保证安全的前提下清理掉坏提交历史改写是 Git 里最需要谨慎的操作。我经历过的项目里因为误提交了一个包含敏感信息的文件后来全团队花了一晚上重写历史。这里要提醒一句改写历史的本质是生成一批全新的提交对象所有基于旧历史的远端、MR、评论、通知都会失去对应关系所以绝对不能对多人共用的主干分支做清理。正确做法是先在本地专门开一个分支来做清洗。常见工具是交互式 rebase可以把某几个提交里的多余文件移除或者用专门的过滤命令对全部历史做统一处理比如删除某个路径下的所有文件、替换提交者信息等。我个人的心得是在大仓库、历史清理这类操作上宁可慢也不要狂。每次执行之前先把仓库做了一个完整的备份直接复制整个目录或者打包成归档文件然后记录好当前 HEAD 的提交哈希。操作完成后用git fsck --full检查仓库完整性确认无误再考虑强制推送。7. 常见问题速查表与我的几条实战经验7.1 一张表快速定位问题这里把前面提到的问题集中整理成一张速查表方便大家在实际操作里快速对照症状可能原因优先处理方案merge 执行到一半不想合并了冲突未解决或选错时机未提交前用git merge --abort退出rebase 过程中连续冲突每个提交应用时都可能冲突逐个解决后git rebase --continue想撤销本地提交但保留改动提交刚刚完成未推送git reset --soft HEAD~1或默认 mixed提交已推送且多人协作不应改变历史用git revert生成反向提交误删分支找不到提交了引用丢失但对象仍在git reflog找哈希重建分支推送被拒 fetch first远程有本地没有的提交先 fetch 再 rebase 或 merge解决冲突后推送换机器凭证丢失凭证存储方式不对配置对应的 credential helpersubmodule 内容不更新主仓库指针和子模块 HEAD 不一致进入子模块目录强制 checkout 到主仓库记录的提交stash pop 冲突目标分支状态与 stash 冲突保留 stash 记录手改冲突文件后再stash drop仓库体积过大大文件或历史对象堆积找出大对象用 LFS 或历史改写清理7.2 几条我踩过坑之后总结的实战经验第一不要在精神状态差的时候执行git reset --hard或者任何带--force的指令。这个听起来像玩笑但我身边真的有人在加班到凌晨三点的时候本来只想 reset 一个文件结果把整个分支推到了旧状态。Git 不会因为你困了就问你再确认一次。给自己设一条硬规矩所有不可逆操作放在精力充足的时间段做或者至少执行之前先按CommandC或者CtrlC停一下深呼吸把命令再读一遍。第二每次开始一个相对复杂的 Git 操作之前先记录当前的仓库状态。常用命令是git status、git log --oneline -5、以及git reflog -5把这三条输出贴在当前项目的问题记录文档里。这样做的好处是不管操作过程中发生什么意外你都知道自己从哪里来、经历了什么最后也总能把状态拉回正轨。我遇到的大部分“事故”最终都是靠 reflog 找回来而 reflog 只有在你事先清楚当前状态时才能发挥最大价值。第三遇到问题先查看报错而不只是复制报错去搜。很多 Git 报错信息本身已经明确告诉了你下一步该怎么做比如“git pull ... to update”或者“use --continue to resume”只是我们太习惯它出现时直接跳到搜索引擎。我现在的习惯是先把报错英文读一遍再根据提示尝试一次很多时候根本不用搜索Git 自己会把答案摆在面前。写在最后的一点体会维护这份 Git 问题记录让我真正理解了一件事工具的问题大多数时候不是工具本身的问题而是我们对工具底层模型理解得不够。分支、提交、引用、对象库这四样东西想清楚了Git 所有表面上的怪异行为都会变得顺理成章。我也不再追求背下所有命令而是遇到问题时能快速归类、快速检索、快速定位这比什么都重要。这份记录还在持续更新因为 Git 本身在迭代我们的使用场景也在变。如果你也有自己的 Git 问题记录或者在这篇文章里看到了你曾经踩过的坑欢迎交流。没有哪一份经验是绝对完整的但每多记录一个坑未来的自己就会少熬一次夜。