
周五下午新来的同事小张在工位上喊了一声“完了”我过去一看屏幕上赫然躺着一串这样的字符 HEAD他扭头看着我眼神里写满了“我是不是把公司代码搞坏了”。我拍了拍他肩膀说没事你没搞坏任何东西你只是遇见了几乎所有程序员都会遇见的东西——Git 冲突。那一刻他确实崩溃了但真正让他崩溃的不是冲突本身而是他不知道眼前这一堆符号到底是什么、该点哪个、会不会把别人的代码覆盖掉。这篇文章就把 Git 冲突这件事从头到尾讲透冲突是怎么产生的、屏幕上那五个符号分别代表什么、到底应该怎么改、以及怎样做才能从源头减少冲突。如果你也曾在面前手足无措这篇内容就是为你准备的。1. 五个符号看懂Git冲突新人崩溃的根源拆解1.1 冲突的本质不是Git坏了是两段代码“撞车”了先说结论冲突不是错误不是你的操作有误也不是 Git 闪退抽风。它是版本控制里一种极其正常的协作状态意思是——两个人或者你自己在两个分支上在同一文件的同一行附近做了不同的修改Git 不知道应该听谁的于是把决定权交给你。打个比方。你和同事合写一份报告你把第三段的“销售额”改成了“营收额”同事同一时间把第三段的“销售额”改成了“GMV”两份修改都保存了最后合并时负责人发现原文被改出了两个版本到底用哪个他不敢替你们决定只能把两个版本都摆出来让你们自己谈。Git 冲突就是这个“摆出来”的过程。很多新人崩溃的第一反应是“我是不是要重写整个文件”其实完全不用。Git 只会在真正有冲突的地方停下来文件里没冲突的部分它早就自动合并好了你只需要处理有争议的那几行而已。1.2 冲突长什么样终端里那些让你懵掉的提示当你执行git pull或git merge遇上冲突时终端会给出类似这样的反馈Auto-merging src/App.js CONFLICT (content): Merge conflict in src/App.js Automatic merge failed; fix conflicts and then commit the result.这句话的翻译是src/App.js这个文件 Git 自动合并不了需要你手动干预。紧接着你用编辑器打开对应文件就会看到那一排刺眼的尖括号。冲突标记一共有五个部分每个都有明确含义 HEAD 当前分支你所在分支的代码 被合并进来的分支比如 origin/main的代码 origin/main中间的是分界线上方的 HEAD那段属于你下方的 分支名那段属于对方。你需要做的就是决定保留哪部分、删掉哪部分或者把两段重新组合成一段正确的代码最后删掉所有标记符号。1.3 为什么“HEAD”成了压垮新人的那根稻草热词标题里说的“看到HEAD的那一刻崩溃了”我太理解这种感受了。崩溃通常不是因为看不懂英文而是因为心理压力怕删错、怕覆盖同事代码、怕引起更大的事故。再加上很多同学是在周五傍晚、需求紧急时第一次遇到冲突深呼吸都来不及。事实上HEAD只是一个指针它指向你当前所处的提交位置。在冲突标记里出现 HEAD仅仅是在告诉你这段是“你这边”的代码。它不神秘不可怕就像一个快递单上写的“寄件人”而已。把心态放平再往下看一切都会简单很多。2. 解决冲突的实操流程从IDE到命令行全覆盖2.1 先用IDE的人性化界面VS Code和WebStorm都行如果你用的是 VS Code遇到冲突文件时编辑器顶部会直接显示一个类似“当前更改”和“传入更改”的对比面板并提供几个按钮Accept Current Change保留当前、Accept Incoming Change保留传入、Accept Both Changes两个都要。这个图形化操作对新人极其友好基本思路如下先打开冲突文件看那段彩色高亮的区域判断上方深色代码和下方浅色代码谁是对的点对应的保留按钮或者手动把两段内容整合成新代码保存文件把冲突标记清理干净。如果你用 WebStorm 或 IntelliJ IDEA它的合并窗口会更强大能左右两栏对比代码中间栏是你的合并结果甚至能直接右键选择“从左边拿”、“从右边拿”。对刚从学校出来、还没有适应命令行操作的新人来说我建议第一优先级用 IDE 图形化解决先把流程跑通。2.2 命令行派的做法一套稳定的四步流程作为常年用终端的开发者我习惯的流程非常固定一共四步git status先看哪些文件冲突了状态会显示为both modified。然后逐个打开冲突文件手动编辑到满意状态再执行git add src/App.js git commit -m resolve merge conflict in App.js提交之后就完成了。整个过程中最容易出错的是第三步很多人改完文件忘了git add直接就很困惑“为什么 commit 不让我提交”其实那只是 Git 在提醒你还有未暂存的内容。记住改完代码 → add → commit顺序不能乱。2.3 没把握的时候留条退路很多人不敢动手改冲突文件是怕改了之后后悔、又不知道怎么撤销。好消息是Git 给你留了不止一条退路。如果你在合并过程中改到一半发现越来越乱想反悔直接执行git merge --abortGit 会立刻把整个分支退回到git merge之前的状态冲突标记全部消失一切恢复原样。如果是 pull 导致的冲突用git merge --abort同样能退出合并状态。这条命令相当于“撤销牌”只要还没提交你就永远有后悔的机会。另外还有一种更精细的“偷懒”操作如果你只看了一眼就知道自己这版不需要、直接用对方的版本就行可以执行git checkout --theirs src/App.js或者反过来保留自己的版本git checkout --ours src/App.js注意--theirs和--ours里的方向在 merge 过程中ours是你当前所在分支theirs是你要合并进来的分支。这个命令能让你跳过繁琐的编辑过程直接把冲突文件整体替换成某一方的版本。但稳妥起见我建议新人先看懂冲突内容再用这个不然很容易把同事的修改一起冲掉。2.4 一段真实的冲突处理全过程光讲命令太干给你还原一次我真实的处理过程。当时我在feature/login分支上改了userService.js同一个文件 main 分支上也改了。执行合并后终端报冲突我打开文件看到 HEAD const DEFAULTS { lang: zh-CN, theme: dark }; const DEFAULTS { lang: en-US, theme: light }; main我先想清楚需求这次上线需要统一改成中文所以保留zh-CN但主题要跟随系统设置所以两段都不完全对。最后我改成const DEFAULTS { lang: zh-CN, theme: system };然后删掉所有的标记行保存文件git add userService.jsgit commit。全程不超过两分钟而且结果比原来任何一版都合理。这就是解决冲突的正确姿态不是“二选一”而是把合并看成一次重新构思的机会。3. 减少冲突从源头抓起团队协作的几条实战经验3.1 永远不要在一个分支上闷头写太久我见过太多新人冲突本质原因是同一个分支开出来后一周都没同步过主干自己闷头快乐地写功能中间主干已经被其他同事推进了好几个版本。等要提合并时撞车地动山摇。我给你一个非常实用的建议养成“一天一同步”的习惯。每天上班第一件事git pull --rebase或者至少隔半天拉一次分支的最新代码。拉得越勤冲突范围越小——冲突大概率只发生在最近几个小时别人刚改过的地方你改你的他改他的基本相安无事。别等攒了一周再合并那时候十几个文件的冲突堆在一起谁见了都头疼。3.2 大文件格式化是冲突第一元凶除了“憋太久”另一个高频冲突来源是格式化。尤其是 Java、JavaScript 这类项目里突然有人给一个几百行的公共文件统一加了格式化整个文件几乎每一行都变了。等合并时你改的那 3 行和格式化后的那几百行全部冲突场面极其恐怖。所以我的原则很简单不要顺手去格式化跟自己需求无关的大文件。IDE 里把这个文件关闭自动格式化或者统一团队用同一个格式配置比如前端项目用.prettierrc后端用统一的 code style。不然你每格式化一次大文件下一步合并就是修罗场。3.3 任务拆解和代码评审是隐性润滑剂减少冲突还有两个“软手段”一是任务拆得越细越好两个人同时改同一份代码的概率自然就低二是请别人评审代码时要求对方“定期把你的分支拉到本地跑一遍”这能让潜在问题在冲突发生前就暴露。我所在的团队有个土方法每天中午讨论时每个人都说一下“我今天会碰哪些文件”如果发现有两个人盯上同一个文件当场就商量谁改上半部分、谁改下半部分。别小看这五分钟的口头沟通它比任何 Git 技巧都管用。3.4 善用 rebase 还是 merge团队定了就不要变关于“用 rebase 还是 merge”网上的争论能写一万篇文章我不打算展开只给你一个最实际的结论听团队的团队用什么你就用什么。如果团队约定用 rebase 让提交历史保持线性那你就每天git pull --rebase。如果你的团队习惯用 merge 保留合并记录来追溯上下文那你就老老实实git pull。两种策略都能减少冲突最怕的是团队里一半人 rebase 一半人 merge最终历史像一团乱麻比冲突本身还难清理。要提醒一句rebase 会把你的本地提交一个个“重放”到最新主干上期间可能每一步都报冲突解决时要把每一条都过一遍对新人来说压力略大。如果团队允许新人前期先用 merge 熟悉流程之后再用 rebase 优化历史不丢人。4. HEAD家族与高频命令一次讲透关键概念4.1 HEAD、HEAD~和HEAD^到底差在哪解决完冲突很多新人会对标题里的HEAD产生好奇它到底是什么跟HEAD~、HEAD^、detached HEAD又是什么关系简单理解HEAD是一个指针永远指向你当前工作位置对应的提交。你切换分支时HEAD就跟着切走。它就像一个书签帮你记住“我读到了哪一页”。HEAD~1表示当前提交的父提交也就是上一个提交HEAD~2是上上个提交依此类推。HEAD^和HEAD~1在绝大多数场景下等价都表示上一个提交区别只在有多个父提交的 merge 提交时才有意义。至于detached HEAD游离的 HEAD指的是你没有在任何分支上而是直接git checkout到了一个具体提交。常见于你想看历史版本代码时敲了git checkout 某个commit哈希。这时候你会看到HEAD detached at的字样此时做的提交不会归属于任何分支一不留神切走就找不回来了。我的建议是新人想查看历史代码不要直接 checkout 提交哈希换成git log里的代码浏览或 create branch at this commit避免丢东西。4.2 用 git commit --amend 修正提交但别乱动公共提交热词里还出现了git commit --amend这确实是个高频又容易踩坑的命令。它的作用是修改最近一次提交可以改信息也可以把新修改的文件并进上一次提交里。典型用法你刚提交完一个更改又发现漏了一个小文件或打错了一个字不想新增一条丑陋的提交记录于是git add src/utils.js git commit --amend --no-edit--no-edit表示保留原来的提交信息不额外弹编辑器。执行完后上一次提交就被改写了。注意这条命令会改变提交的哈希也就是说它“改写历史”。如果你上一次提交已经 push 到远程并且别人已经拉下来了就不要再用 amend 了否则你的历史和别人的历史会分岔接下来又是一场撕扯。简单原则还没 push 的提交随便 amend已经 push 的公共提交老老实实新建提交。4.3 分支合并的进阶配合squash合并和cherry-pick最后简单说说两招进阶操作。当你的功能分支已经提交了十几个零碎记录比如“改格式”“改注释”“改错别字”这种合并进主干时想让历史干净一点可以用git merge --squash feature/login效果是把整个分支的修改压缩成一个改动暂存到工作区然后你手动提交一次。历史一下子清爽了。还有一个很实用的场景只想把某个分支上的某一个提交应用到当前分支不需要整个分支合并。比如别的同事偷偷修了一个 bug你不想等他的分支合并可以直接挑走那一个提交git cherry-pick commit-hash这个命令在热词里没出现但它是分支协作的重要补充建议新人提前了解一下遇到“只想要别人那一个改动”的场景能救急。5. 冲突处理完别慌着收工提交之后的收尾检查5.1 验证代码真的能跑起来很多新人解决完冲突把标记一删、提交一推就觉得大功告成结果 CI 上炸成一片缩进错了、变量名重复、两段逻辑叠加后出现奇怪的行为。冲突解决不是“去掉标记符”就完事而是一次人工代码合并必须有验证环节。我的习惯是解决完一个文件的冲突先在本地把相关功能跑一遍。如果改动涉及路由就启动服务走一遍页面如果涉及纯函数就跑一遍测试用例。至少确认一下没有语法错误。这一步花不了几分钟但能挡住绝大部分“假解决”的坑。5.2 确认没有遗漏的冲突标记还有一个小技巧全文搜索一下或。如果一个文件里冲突点特别多人眼高亮很容易漏搜索是最高效的兜底。VS Code 里按CtrlShiftF全局搜索或者用命令行grep -rn .结果为空才说明文件真正干净了。我曾经见过有人提交时把一个留在字符串里当成代码的一部分最后造成了非常诡异的行为排查了一整个下午。搜索这一步真别省。5.3 提交信息要写得让人看懂来龙去脉最后解决冲突的那次提交提交信息最好不要只写一句merge。我通常写resolve conflict in userService.js, keep zh-CN default and merge theme config这样同事和未来的你回看历史时能立刻知道这次冲突发生在哪、最终处理原则是什么。很多人只把 Git 当存档工具却忘了它也是团队的沟通工具。冲突处理完顺手在提交信息里交代清楚团队协作的体验会好很多。说到最后我想起那年我第一次遇到 HEAD时也曾在屏幕前发懵。但现在回头看那次冲突反而帮我真正理解了 Git 的合并机制。新人在 Git 面前崩溃不可怕可怕的是从来不去弄懂那五个符号在说什么。掌握这些以后你会发现冲突其实是一次深入了解代码的机会——你要同时读懂自己写的和别人写的两份逻辑再把它们融成更好的那一份。