
就在几秒前你还在愉快地写着代码git pull敲下去终端刷出一行不起眼的提示然后你打开文件一看屏幕正中央躺着一排神似乱码的符号 HEAD那一瞬间很多新人程序员的第一反应是我是不是把代码改坏了是不是不小心按到了什么快捷键这个符号怎么这么像小时候写作文用的删除线说实话我见过不止一个实习生在这个画面面前沉默三分钟然后默默把电脑转过来问我“这个……是不是我的东西全没了”不是你东西都在。不光是你的东西在别人改的东西也在Git 只是把它们俩都摆在了你面前并且用一行行尖括号告诉你这里发生了冲突。这不是什么罕见事故而是每一个用 Git 的程序员迟早都会遇到的家常便饭。这篇内容我想从那个让新人崩溃的 HEAD讲起把冲突标记的含义、冲突从哪来、怎么一步步解决、以及怎么让自己以后尽量少碰到这种场面完整地过一遍。文章里的场景和命令都是我在真实项目里踩过、用过的你可以直接照着操作。1. 先把崩溃现场拆开看冲突标记到底在说什么1.1 那七个小箭头不是乱码而是 Git 在给你指路先看一个标准的冲突文件片段假设你正在改一个叫config.js的文件pull 之后打开它里面长这样// config.js const apiUrl https://api.example.com; HEAD const maxRetries 3; const timeout 5000; const maxRetries 5; const requestQueueEnabled true; feature/login如果你第一次见这个会觉得这玩意儿是文件被污染了。但实际上Git 是在用最朴素的方式告诉你两件事 HEAD到之间的内容是你当前分支HEAD 指向的地方里的版本。这里的 HEAD 指的是你本地当前所在分支的最新一次提交。刚才你 pull 之前自己 commit 进去的修改就在这个区间里躺着。到 feature/login之间的内容是你 pull 下来的另一个分支或者远程分支里的版本。feature/login就是那个分支的名字。连起来读就是你对这段代码改了一版同事在feature/login上也改了一版Git 不知道你们俩谁说了算于是把两个版本都塞进同一个文件然后画了一条分界线让你来做这个决定。这就像你和室友共用一张购物清单你手写了一圈“买牛奶”你室友在旁边拿红笔批了个“买酸奶”两个人各写各的最后这张纸上被圈出两块内容。Git 干的就是这么件事只不过它没有红笔只好用尖括号当荧光笔。这里有个细节值得新人注意、、这些符号在 Git 里被叫做“冲突标记”conflict markers。它们不是编译错误也不是你代码里的 bug而是 Git 用纯文本方式留在文件里的标记。Git 本身是一个命令行工具它没办法弹一个漂亮的窗口问你“你想保留哪个”所以它把决策信息直接写进了文件里等你处理完把这些标记行删掉再告诉 Git“我搞定了”整个流程才算结束。1.2 为什么 Git 不帮我自动选一个非要让我崩溃很多人会有一个很自然的疑问既然 Git 这么强大为什么它不直接帮我从两个版本里挑一个更好的非要搞出一堆尖括号让我自己看答案是Git 不是不会挑而是不敢替你挑。它能自动合并掉大部分冲突但有一种情况它一定会停下来就是两个人改了同一个地方而且改的方式各有各的道理。我们拿上面那个例子来说。你的版本是maxRetries 3同事的版本是maxRetries 5。如果 Git 直接帮你选了 3那同事在feature/login上写的requestQueueEnabled true这个新逻辑就会因为整块内容被覆盖而丢掉。反过来如果 Git 帮你选了 5那你本地调试了很久的超时参数就白调了。这就是问题的核心两个修改都体现了两边的意图而这些意图在语义上是冲突的Git 作为一个没有业务理解能力的工具没办法判断哪个才是项目真正想要的。我见过很多新人会用“垃圾 Git”来评价这次经历但如果你换一个角度想这恰恰是 Git 最负责任的地方。它宁可停下来多问一句也不愿意悄悄覆盖掉任何一个人的工作成果。在多人协作里静默丢失数据才是真正可怕的事冲突至少是明着告诉你“这里有分歧”。况且 Git 也不是什么都不能做。如果两个人改的是同一个文件的两个不同区域甚至是两个不同的文件Git 基本都能自动合并掉整个过程你都感觉不到冲突的存在。只有改动重叠了它才会动用冲突标记。换句话说看到 HEAD不是常态反而是 Git 尽了最大努力之后实在兜不住底才会亮出来的红灯。2. 冲突到底怎么来的从一次 Pull 说起2.1 典型翻车现场还原先上一段我在带新人时经常复盘的真实场景所有人物都用化名但过程完全真实。项目组里 A 同学和 B 同学任务分到了同一个模块的不同需求上。A 的任务是在用户列表页加一个搜索框B 的任务是在用户列表页加一个状态筛选。两个人从主干分支上各自拉了一个新分支开始干活只不过中间隔了两天。A 的动作是这样的git checkout -b feature/search # 改了 user-list.js git add user-list.js git commit -m add search boxB 的动作是这样的git checkout -b feature/filter # 也改了 user-list.js git add user-list.js git commit -m add status filter改的还不是同一行不问题就出在两个人改的地方离得太近了。A 在组件顶部加了搜索框的 state 和输入框模板B 也在组件顶部加了筛选条件的 state 和下拉框模板。两边插入的位置正好是同一个区域甚至有一行是紧挨着的。A 后来先把自己的分支合到了主干然后推到了远程。B 这时候想把自己的分支也合并进去于是在本地执行了这样一条命令git pull origin main因为 B 本地已经有了一个feature/filter分支上的提交远程的main上又有 A 新合进去的提交两条线在这里分叉了。Git 尝试把远程main的内容合并进 B 当前所在的分支结果一比对发现user-list.js文件的顶部两个人都在同一个位置做了插入操作自动合并失败。Git 没有立即报错退出而是把冲突标记写进了 B 的工作目录里的user-list.js然后在终端里显示Auto-merging user-list.js CONFLICT (content): Merge conflict in user-list.js Automatic merge failed; fix conflicts and then commit the result.B 打开文件屏幕上出现了那排刺眼的 HEAD。所以过程串起来就是fetchB 拿到了远程 main 的新提交→ mergeB 本地尝试把远程 main 合并到当前分支→ 冲突两个人都改了同一个文件的同一块区域Git 无法自动合并→ 文件被写入冲突标记 → 等待 B 手动处理。这就是大多数新人第一次见到冲突标记的标准路径。2.2 冲突的三类常见来源不只 merge很多人以为冲突只有git merge才会产生其实不是。凡是涉及 Git 内部“三方合并”机制的操作都有可能把冲突标记带进你的工作区。我按实际碰到过的频率给你排个序。第一类多人协作时最常见的git pull触发隐式合并。严格来说git pull等价于git fetch加git merge如果你的本地分支和远程分支都产生了新的提交那 pull 的时候就等于在执行一次合并合并失败就会出现冲突。这是新手遇到冲突的头号入口。第二类git rebase中途被杀停。rebase 的理念是“把你的提交一个个搬到别人的最新进展上去”每搬一个提交Git 都要做一次三方合并。如果某个提交改了某个地方而目标分支的同个地方也被改了冲突就会在 rebase 的过程中冒出来。这时的冲突标记长得很像 merge 的但区别在于左边的内容可能是“对方分支的新版本”而右面写的是“你正在被重放的某个提交的编号”因为正在移动的提交还没有名字Git 只能用一个 commit hash 来标记。第三类容易被忽略的git cherry-pick、git stash pop、git pull --rebase也都会触发合并逻辑。cherry-pick 是把别人的一个提交“摘”到你当前分支stash pop 是把你暂存的修改重新应用回工作区这些操作本质都是把一批修改叠加到当前代码上一旦叠加的位置有冲突Git 照样会写下 HEAD。所以完全可以把冲突理解为只要是“把一组修改应用到另一组修改之上”的操作都存在冲突的可能性。这不是某个命令的锅而是文本合并的天性。它意味着你不需要在每次碰到冲突时都陷入自我怀疑因为这是分布式版本控制系统的结构性特征躲不掉只能学会处理。3. 手把手解决一次冲突完整实操流程3.1 冲突信号怎么识别遇到冲突之后第一件事不是慌而是确认到底是不是冲突以及有哪些文件冲突了。如果你在终端里看到关键字CONFLICT说明这次合并没有走完。绝大多数时候当前分支已经处于一个“合并中”的中间状态。你运行git status如果输出里有一行是Unmerged paths: (use git add file... to mark resolution) both modified: src/pages/user-list.js那就没跑了both modified就是指这个文件在两边分支上都被修改了现在需要你来裁决。如果你用的 IDE 是 VS Code 或者 JetBrains 系的编辑器冲突文件在文件树里通常会用特殊的颜色和图标标出来打开后冲突区域的背景色会被高亮比纯终端直观得多。记住一个关键知识点在有冲突待解决时你当前的状态是“合并进行中”还没有形成一个新的提交。这个状态下你最好不要急着去动别的文件更不要直接 commit因为你的提交内容不完整Git 会拒绝。所谓“解决冲突”完整地说就是把每个冲突标记里的内容手动裁决好、删掉标记行、然后git add把文件标记为已解决最后再git commit把合并结果固化下来。3.2 从编辑文件到提交的完整步骤假设你手头就是一个上面那种config.js的冲突我一步一步演示给你看。这个过程我建议新人至少在测试仓库里亲手跑一遍做完之后以后再碰到心里就有底了。第一步打开冲突文件定位所有冲突标记。一个文件里可能有多个冲突区域每个区域都是同一套结构开头、分界、收尾。用编辑器搜索可以快速跳到下一个冲突点。第二步逐个区域裁决内容。拿下面的冲突来说 HEAD const maxRetries 3; const timeout 5000; const maxRetries 5; const requestQueueEnabled true; feature/login你需要做的就是决定最终保留什么。如果这个项目的需求是“重试 3 次并且加超时控制”那你就保留左半边的内容删掉和 feature/login之间的maxRetries 5和requestQueueEnabled true如果业务上更看重重试 5 次和请求队列那你就保留右半边如果两边都想要那就手动拼一个正确的版本把两者中不冲突的部分合并在一起。比如最终结果可能是const maxRetries 5; const timeout 5000; const requestQueueEnabled true;这种“都保留”的情况非常常见因为很多时候两边改的其实不同维度只是恰好落在同一个代码块里。关键就一句话把文件改成你想要的样子确保里面没有任何一行、、残存并且语义正确。第三步保存文件退出编辑器回到终端。运行git add src/pages/user-list.js这一步的意思是告诉 Git“这个文件我已经手动处理完了冲突解决了。”注意git add在这里不是暂存新功能而是“标记冲突已解决”这是 Git 协议里规定好的流程。第四步提交合并结果。直接运行git commitGit 会打开一个编辑器里面预填了一条提交信息通常长这样Merge branch main into feature/filter # Conflicts: # src/pages/user-list.js你不需要在这条信息里写太多保留它默认的内容甚至直接保存退出即可。这一步生成的提交就是这次合并的正式结果里面会同时包含两边的改动历史。3.3 解决冲突时的“三问法”命令层面的东西说完了但如果你真的去处理一个复杂冲突会发现最难的从来不是敲命令而是“我怎么知道哪个版本是对的”。我在帮新人排解这个困惑的时候总结了一个简单好记的决策流程我叫它“三问法”。第一问这一行是谁改的为什么改的如果你眼前这个区域是你自己前两天刚写过的东西你大概率还记得当时的业务背景如果是同事改的那你可能需要看看git log或者git blame找到对方的提交和提交信息。提交信息写得好不好在这时候直接决定了你排查冲突的效率这也是我一直强调提交信息要写清楚“为什么”的原因之一。第二问两边的改动在语义上是互斥的还是并存的互斥的情况就是同一份配置只能选一个值比如超时时间到底是 3000 还是 5000并存的情况就是两边动了同一块区域的不同逻辑你需要做的只是把两边的内容“拼”起来而不是“选”一边。很多人解决冲突时喜欢偷懒直接选一边放弃另一边这往往是 bug 的来源。第三问如果两边都保留会不会产生重复定义或者逻辑冲突也就是从“选 A 还是选 B”升维到“A 和 B 能不能一起活着”。这一步需要你对项目结构有基本认知比如两个人都往同一个数组里 push 了不同类型的对象你保留两边的话这个数组可能就需要同时兼容两种结构你要检查后续使用的地方是否兼容。说实话处理冲突这件事80% 靠的是代码阅读能力20% 才是 Git 命令熟悉度。新人容易崩溃不是因为 Git 命令不熟而是因为他们在不知道自己改了什么、也不知道别人改了什么的情况下被要求对着一堆符号做裁决。所以我的建议是动手之前先看清楚再动手。3.4 需要避开的坑有些坑我几乎每个季度都能在团队里看到一次列出来提醒你避雷。第一个坑把冲突标记当代码提交上去了。有人手动操作的时候漏删了一行 HEAD自己没检查直接git add、git commit、git push。然后整个团队的代码里就混入了尖括号一旦别人编译不过或者打开文件看到这串符号第一反应就是找你“算账”。所以提交之前务必全文搜索一遍这个习惯我用编辑器里的全局搜索就已经养成了。第二个坑只解决了一部分冲突就急着提交。一个文件里有多个冲突区域你只处理了第一个后面的还没看就直接 commit 了。这时候 Git 会提示你“you have unmerged paths”提交会被拒绝。但如果你用了git commit -a或者某些 IDE 的“提交全部”有可能会把还没处理的文件一起带上去。稳妥的做法是提交前再看一眼git status确认没有任何Unmerged paths。第三个坑还没搞清状况就乱用git checkout --ours或--theirs。这两个参数确实可以快速把冲突文件替换成某一方的版本但它们的行为在 merge 和 rebase 下是相反的而且“一键替换”会把另一方的全部改动都丢掉危险系数很高。我建议新人别着急用先手动解决个五六次理解了合并逻辑之后再用这类快捷方式。第四个坑忘了还有后悔药。如果你发现自己越解越乱完全可以停下来回到合并之前的状态git merge --abort这条命令会取消这次合并把工作区恢复到执行 merge 之前的样子。如果你是因为 rebase 卡住的用的是git rebase --abort。这个操作是最后的保险但它只能用在合并中途一旦你提交了合并结果就没办法简单撤销了。所以我个人的习惯是在处理冲突之前先把当前的分支名字记下来或者在关键节点上打个临时 tag留条后路心里踏实。4. 实战中常见的几种冲突形态与对应解法4.1 两边改的是同一行这是最直白的冲突也是最让新人头疼的。场景往往是这样A 把某个函数里的默认参数从5改成了10B 在同一行附近把这个函数的注释改成了一行新的说明。因为两边的改动在文本上靠得太近Git 的三方合并算法认为这一行“同时被两个人动了”于是产生冲突。这种冲突的解法通常很简单看业务上需要哪个值选中一边即可。真正需要注意的是如果你看到冲突区域里的两段内容几乎一样只有细微差别那大概率是“重复改动”这时候最好跟同事说一声确认一下谁保留谁的版本避免直接合并出一个“你一行、我一行的缝合怪”。4.2 一边改了文件另一边删了文件这种情况不同于普通的文本冲突Git 会提示类似CONFLICT (modify/delete): src/utils/legacy.js deleted in feature/cleanup and modified in HEAD.一边分支把文件删了另一边分支把文件改了。Git 不知道该听谁的是保留改动还是尊重删除这时候你就得判断这个文件还有没有用。如果功能已经废弃那就接受删除如果还是有用的逻辑那就保留改动。操作上你需要手动决定后用git add或者git rm把这个文件标记为已解决然后继续合并。这个坑特别容易出在“代码清理”和“功能开发”并行的时候。一个分支在打扫卫生把用不到的公共文件删了另一个分支刚基于这个公共文件拓展了新功能。两边都觉得自己没错最后就得有人出来做仲裁。4.3 代码格式化导致的“假冲突”这可能是最冤的一种冲突。A 的编辑器配了保存时自动格式化把整个文件的缩进从两个空格改成了四个空格顺手把引号从单引号统一成了双引号。B 在这个文件里加了一行简单的日志代码。结果 A 这个格式化的提交一合并B 那行日志所在的整个代码块都被判定为冲突哪怕 B 的改动本质上只有一行。这种冲突几乎没什么技术含量处理起来也最难受因为冲突区域可能横跨几百行但真正的分歧只有一行。应对方法有两个层面短期的层面如果只是引号或者缩进变化你可以选择某一方的整个版本再手动把对方的真实改动重新应用进去长期的层面团队里最好统一格式化工具和配置比如全组用同一个代码格式化器并把格式化的配置提交到仓库里不然这种“格式化冲突”会像过敏一样反复出现。4.4 冲突文件特别多或特别长怎么办遇到一次合并带来十几个冲突文件每个文件里还有七八个冲突区域新手很容易当场石化。我的建议是把问题拆成两步走先治标再治本。治标就是按“风险等级”分批处理。先处理那些内容简单、一眼能看懂的冲突比如同一行值不同、注释冲突用最快速度解决掉。把明确的先扫干净剩下那些逻辑交织、需要翻业务代码的复杂冲突集中精力慢慢看。这个过程中git status是你的进度条它能实时告诉你还有哪些文件是 unmerged 状态。如果冲突文件本身特别长比如一个几千行的文件被格式化和业务改动搅在一起最实用的办法反而是找最原始的共同版本做基准然后手动把改动重放一遍。用git log找到两边分支分叉的共同祖先提交再git diff这个共同祖先和两边分支的差异能帮你理清哪些改动是真正该有的哪些只是噪音。本质上冲突就是在“三方合并”里的三方共同祖先、你的分支、对方分支。当你把共同祖先拉出来看很多混乱就会立刻明朗。为了让你以后查起来方便我把这些形态整理成一个速查表建议收藏冲突形态典型提示处理思路两边改动同一行或同一块CONFLICT (content)看业务语义选一边或手动合并一边改一边删CONFLICT (modify/delete)判断文件是否还有用用 add 或 rm 解决格式化引起的批量冲突CONFLICT (content)区域很大统一格式化配置短期手动重放改动二进制文件冲突CONFLICT (binary)Git 无法自动合需要人工选择保留某个版本重命名冲突CONFLICT (rename/delete)确认重命名后的文件与内容归属5. 日常如何少踩冲突更聪明的协作姿势5.1 提交前先看看别人改了啥冲突的核心前提是“两条分支在同一个地方产生了分叉改动”。那一个很直接的减少冲突的办法就是别让你的分支落后主干太远。我见过很多新人习惯埋头干一周的活攒一个巨大的 commit然后一次性 pull、push结果一次 pull 撞出五六个文件冲突全是这一周里同事改过的。这种工作方式不是不行但它把冲突的代价集中到了一个时间点处理起来的心理压力也最大。更好的姿势是在开始一天的工作或者准备提交之前先同步一下远程主干的最新代码。有两种同步方式默认的是git pull等于 fetch 加 merge还有一种是git pull --rebase它可以把你本地还没提交的 commit 先收起来把远程的最新提交拉下来再把你自己的提交一个个重新放上去。用 rebase 的好处是提交历史是线性的看起来更干净坏处是如果遇到冲突你要逐个处理自己每一个 commit 里的冲突可能很繁琐。个人建议是如果你在一个多人协作的活跃仓库里且本地分支已经落后较多用git fetch看看远程的变化再决定用 merge 还是 rebase不要盲目操作。尤其注意一点在一个已经推送到远程、多人共用的分支上不要随便 rebase因为 rebase 会改写历史可能会把别人的协作历史搅乱。5.2 小步提交与清晰提交信息新手向资深程序员进化一个很重要的分水岭就是对 commit 粒度的理解。刚入职的人经常一天下来就一个 commit里面的改动可能横跨三个文件、五个功能点干得久的人会倾向于把改动拆成小而完整的提交一个功能一个提交一个 bug 修复一个提交一个重构单独一个提交。这和减少冲突有什么关系关系非常大。冲突的规模往往和 diff 的规模成正比。两个分支各自 diff 越小自动合并成功的机会就越大即使真的撞上了冲突区域也会更小、更容易理解。反过来一个巨大的 diff 撞上另一个巨大的 diff那基本就是一场灾难。另外提交信息写清楚“为什么”而不是“改了点东西”在冲突排查时是救命级别的帮助。比如你跟同事的改动撞了你打开git log --oneline看到对方的提交信息是“refactor user list rendering”你立刻能判断这个提交动了哪些逻辑如果看到的是一堆“update”或“fix”那你只能靠自己去 diff效率完全不一样。所以从今天起写提交信息就当写给三天后的自己看那是你最可能遗忘细节的时刻。5.3 用工具辅助解决冲突命令行处理冲突是基本功但工具能帮你把大量时间省下来。我个人最常用的组合是 VS Code 的合并编辑器它会把冲突区域分成三栏当前更改、传入的更改、合并结果你点一下按钮就能在“保留当前”“保留传入”“两者都保留”之间切换。对于多数简单冲突这个交互比手工编辑标记行快得多也不容易误删内容。如果你更偏好桌面工具Beyond Compare 和 kdiff3 也是老牌的 diff/merge 工具它们能展示更清晰的左右对比适合冲突区域很多、需要反复对照的情形。配置方式是在终端里设置 Git 的 mergetool比如git config --global merge.tool vscode git mergetoolgit mergetool会逐个带你处理冲突文件处理完一个继续下一个对一个文件列表很长的大型合并来说很有效率。还有一个命令行层面的神器我愿意逢人便推就是设置 diff3 风格的冲突展示git config --global merge.conflictStyle diff3开启之后冲突标记里会多出一段被|||||||包围的内容它显示的是两个分支分叉之前、共同祖先里的原始内容。有了这段原始版本作为参考你能一眼看出“这个区域本来是什么样”然后判断两边分别做了什么改动、有没有人把原来的东西改没了。这个设置对解决复杂冲突帮助极大我现在已经离不开了。5.4 团队层面的规矩比技巧更重要讲完个人操作再往上一层说点团队协作的事。很多冲突看起来是工具问题本质上其实是沟通问题。两个人在同一个文件的同一块区域做改动大概率是因为他们不知道对方正在做同一件事。所以我在团队里会推动几件小事第一比较大的重构或者跨模块改动提前在群里或者站会上说一声让大家知道“这块地盘近期有人动工”第二公共文件比如路由配置、公共组件、工具函数库改动前留意一下最近的提交记录看看是不是有几个人同时在碰这些文件第三涉及集中式文件的改动尽量拆开时间不要同一两天内扎堆提交到主干。这几点不需要任何工具就能做到纯粹是工作习惯。但它比任何 Git 命令都更能从源头上减少 HEAD的出现频率。工具解决的是“冲突发生后怎么高效处理”团队习惯解决的是“怎么让冲突少发生”。6. 遇到冲突后的高频疑问速查针对平时被问得最多的问题我整理了一份速查表你可以存着备用。解决问题的顺序往往比问题本身更重要。疑问快速答案如何快速定位文件里所有冲突标记在编辑器里全局搜索或者用grep -rn ^ 目标目录扫描工作区如何完全放弃这次合并回到之前状态如果是 mergegit merge --abort如果是 rebasegit rebase --abort只想保留我这一边的版本怎么写在 merge 场景下git checkout --ours 文件rebase 场景下git checkout --theirs 文件注意两者语义相反如何把某个文件标记为冲突已解决git add 文件然后继续处理下一个文件全部处理完再 commit二进制文件冲突怎么办Git 不会往二进制里写冲突标记只会提示 binary 冲突你只能选择保留某一方的版本手动替换文件后git add合并完的提交信息有模板吗merge 产生的提交会默认使用类似 “Merge branch xxx into yyy” 的预填信息直接保存即可为什么我的冲突里没有分支名而是一串 hash正在 rebase 或者 cherry-pick 时因为还没有分支引用后面显示的是 commit id别人会不会看到我没处理完的冲突标记只要你没 commit 没 push冲突标记只会存在于本地但如果你把冲突文件 add 后直接 commit它就会进入远程仓库这里面我再多说一句git checkout --ours/--theirs。很多教程把它吹成“冲突一键解决”的灵丹妙药但用不好就是灾难。它的本质是从索引里取出某一方的文件版本覆盖你当前的冲突文件它会无视另一方的所有改动。所以只建议在你清楚地知道“这个文件的这个冲突区域我就是要整体用某一方的版本”时使用。对一个刚开始接触 Git 的新人来说手动处理几次再碰它会更安全。最后分享一个我个人的处理习惯处理冲突时手上永远开着一个终端跑git status每处理完一个文件就git add一个然后看一眼状态。这个动作看起来机械但它能让你对整个合并的进度始终心里有数不会出现“以为处理完了结果还差三个文件”的情况。等git status里再没有任何未合并文件时再git commit一次搞定。最后说点实际的说实话我带了这么多届新人见过崩溃的姿势千奇百怪但归根结底都一样不是输给了 Git而是输给了“未知”。当你第一次看到 HEAD的时候它像一个外星信号你不知道它是什么不知道它从哪里来更不知道该怎么回应它。可一旦你搞清楚了它只不过是一份“两个版本摆在你面前、请你做裁决”的清单那它就不再可怕了。我自己的经验是带新人入门 Git 冲突与其花一小时讲原理不如直接让他们在一个测试仓库里故意制造一次冲突然后亲手解一次。冲突这种事说一百遍不如做一遍。处理过第一次、第二次、第三次之后你甚至会对那排尖括号产生一种“老朋友”般的熟悉感——它不是灾难预警它就是 Git 在认真地告诉你这里需要你的判断我信得过你。所以下次再看到 HEAD先别急着崩溃打开文件倒杯水把两边的代码都看一遍决定好了删掉标记addcommit继续干活。这个流程走一遍你就从“被冲突吓到的新人”变成了“处理冲突的老手”。多出来的那点经验全都会在未来的协作里帮你省下大把时间。