Git 冲突完全指南:从理解 HEAD 到优雅解决合并冲突

发布时间:2026/10/6 3:24:08
Git 冲突完全指南:从理解 HEAD 到优雅解决合并冲突 你正带着耳机改代码准备把自己昨天没写完的登录接口收尾习惯性敲下git pull回车。三秒后终端冒出一片红色CONFLICT (content): Merge conflict in src/api/user.js。你点开那个文件看到的不是熟悉的代码而是一排诡异字符 HEAD feature/login那一刻很多新人程序员是真的崩溃的。“看到 HEAD 的那一刻新人程序员崩溃了”这个标题在我看来就是 Git 入门的第一道拦路虎。其实 HEAD 一点也不玄乎你完全可以把它理解成“当前你所在分支的最新提交位置”而接下来要面对的真问题只有一个同一份文件被两个分支同时改过Git 不知道该听谁的所以把两版都摊在你面前让你来当这个裁判。这篇东西就是写给被冲突标记吓到过、也准备以后再遇到时不再慌的人。我尽量把 Git 冲突、HEAD 相关概念、以及我这些年踩过的坑一次讲完。1. 场景复盘那个把新人击溃的“七个小箭头”1.1 冲突标记到底长什么样上面那种由、、三部分组成的内容就是 Git 合并冲突的标准现场。完整看起来是这样 HEAD function getUserInfo(id) { return db.query(SELECT * FROM user WHERE id id); function getUserInfo(id) { return db.query(SELECT * FROM users WHERE uid ?, [id]); feature/login我拆开给你看标记含义它指向的内容 HEAD冲突区开始接下来是“当前分支”里的版本分界线上面是当前分支版本下面是另一个分支版本 feature/login冲突区结束结尾处标明另一个分支的名字很多人第一眼看成“代码被切碎了”也有新人把当成等号赋值把当成某种模板语法。其实这三行字符唯一的作用就是告诉 Git 和你这里有两份不同的修改需要你决定留哪个。1.2 崩溃的心理原因怕丢代码、不敢决策、不知道下一步新人在这个场景下崩溃并不是因为笨而是三个很现实的心理压力同时压过来。第一是怕丢代码。看到“冲突”两个字第一反应往往是“我是不是把什么搞坏了”“会不会把同事的代码弄丢”。实际上 Git 非常保守冲突时它不会自动丢弃任何一边的内容所有参与冲突的分支内容都被原样保留在文件里你的代码一行没少同事的也一行没少只是揉在了一起。第二是不敢决策。当和之间的两段代码都长得有道理时新人往往会陷入选择恐惧“我选左边会不会把同事的功能弄没了我选右边会不会我的功能就白做了”这种犹豫太正常了因为你自己写的代码你会但判断同事那半段是否能保留需要上下文。第三是不知道下一步。这也是最致命的一点。很多人并不知道“处理冲突”之后还需要git add和一次新的提交他们只是把冲突标记删掉就以为完事了结果git status里文件依然是红色的甚至不敢再动任何命令。其实流程很简单改完文件后git add再git commit冲突就正式结束了。这个流程我会在第三章完整演示先不着急。1.3 HEAD 到底是谁先解释一下标题主角 HEAD。它不是一个骂人的词也不是哪个人名它是 Git 内部的一个“指针”指向你当前所在的提交位置。举个生活化的例子你读一本连载小说每次关书之前会夹一个书签下次打开直接翻到那一页继续读。HEAD 就是 Git 的书签只不过它夹的位置永远是你当前所在分支的最新提交。你在feature/login分支上工作时HEAD 就指向 feature/login 的最新提交切回mainHEAD 就跟着指向 main 的最新提交。所以 HEAD的含义很直白这半段代码来自你当前所在分支的这个版本。那个让你崩溃的箭头其实是 Git 在“指着自己人说话”只是它指得比较吓人。2. 追根溯源一次 pull 怎么就碰上了冲突2.1 三个高频操作其实是同一类东西git pull、git merge、git rebase在触发冲突这件事上是同一类东西。pull 是“先拉远程分支内容到本地”然后自动执行一次合并。也就是说git pull的本质是git fetch加git merge所以你遇到的 pull 冲突本质上就是一次 merge 冲突。触发冲突的高频场景有四个执行git pull本地有提交远程也有新提交两边改了同一处代码。执行git merge feature/login把 feature/login 分支合并进当前分支。执行git rebase main把当前分支的提交挪到 main 分支最新提交之后重新播放。执行git stash pop把暂存的修改恢复到工作区但工作区当前内容已经变了。模式都一样Git 发现两段内容不同又都改动在同一行附近它没法自己决定于是把“选择权”交给你。2.2 三方合并Git 不是傻瓜它其实是做过功课的很多人误以为 Git 遇到不同就报冲突这是不对的。Git 做的是“三方合并”它并不是拿两份文件逐字比较而是会找到这两份改动共同的那一个原始版本这个原始版本叫base。我打一个比方。你和同事各拿一份同一个文档的副本原文里有一句话是“今天天气很好”。你改成“今天有太阳”同事改成“今天适合打球”当把两份改完的文档合成一份时有三份材料摆在面前原本的“今天天气很好”、你改后的“今天有太阳”、同事改后的“今天适合打球”。如果你俩改动的内容互不重叠Git 会自动把两份修改都保留只有当你俩在同一个区域都做了修改时它会提示“这个地方我智能不了你自己决定吧”。所以 Git 的冲突不是它算不出来而是它按照规则判断“两侧都修改了同一区域且结果不同无法自动合并”。这不是缺陷而是一种保护机制——宁可让你来做决定也不盲目覆盖任何一方的代码。3. 手把手把一次冲突从“崩溃现场”变回常态3.1 动手之前先做三件小事遇到任何冲突我建议你执行完下面三步再碰编辑器经验之谈能省掉很多后续麻烦。第一看状态。执行git statusGit 会列出一堆both modified: src/api/user.js这样的提示注意看both modified这个词它说明这个文件两边都动了是真正的冲突文件只有单边动的文件 Git 已经帮你合并好了不要乱动。第二确认手头没有没提交的重要改动。冲突发生时工作区里可能还有你正准备提交的半成品代码。稳妥起见先git stash把没提交的东西暂存走处理完冲突后再git stash pop拿回来。如果你怕 stash 也不保险最蠢但最有效的办法把当前整个文件复制一份到桌面备份。第三明确你正在处理的是哪一种合并方式。是在git pull之后的 merge 冲突还是在git rebase过程中的冲突这两种场景下“左侧代码是哪个分支的”方向完全不同我在 3.5 里会专门说。3.2 在文件里找出所有冲突标记文件如果不大直接看上下文就行。如果文件很大几十个冲突标记混在一起不要靠肉眼硬扫用搜索命令grep -rn .这条命令会递归列出当前目录下所有包含冲突标记的文件和行号。还有个小技巧git diff也能看到冲突它会用、、配合、---展示差异但日常还是推荐直接在编辑器里处理。VSCode 里处理冲突很舒服文件顶部会标黄底提示“您有合并冲突”代码区域会出现三个按钮——Accept Current Change保留当前分支版本、Accept Incoming Change保留另一分支版本、Accept Both Changes两个版本都保留。这三个按钮做的事情本质上就是帮你把左边的代码留下、右边的代码留下、或者两边都留下再把冲突标记删掉。用熟了之后你甚至可以不通过按钮直接手动编辑。3.3 逐段做判断该留哪一边这是整个处理冲突过程中唯一没有标准答案的环节。我一般按这几步走先看两个版本各自改了什么。如果两段代码逻辑一样、只是命名或格式不同保留你所在分支那一份也就是HEAD下面的内容然后让同事 rebase 后再对齐命名规范就行。如果两个版本是不同的逻辑那就要搞清楚每一段代码的业务含义必要时直接问写那段代码的同事“你这个改动是为了解决什么问题”我遇到过最典型的例子是两个人都在同一个接口函数里加参数校验A 加了“用户不能传负数”的判断B 加了“字段长度不能超过 20”的判断两边改动落在不同行Git 其实能自动合并真正冲突的是两个人都在同一个if判断里写了不同条件。这种时候不能只挑一边而是把两边条件合并成一个完整的校验逻辑。判断完之后手动删除三个冲突标记行把两段代码整理成一段最终版本保存文件。3.4 收尾提交add commit 不能少所有冲突文件都改完后git status再看一眼确认没有遗漏的冲突标记。如果全部处理完就可以重新暂存并提交git add src/api/user.js git commit -m Merge branch feature/login into main如果是git pull触发产生的 merge 冲突Git 会提前帮你写好一条 merge commit 信息直接git commit不加 -m就行我建议你保留默认的 merge 提交因为它保留了合并来源信息后续回溯的时候一眼能看懂。提交完成冲突就正式结束了git log里会看到一条 merge 提交记录。这里有一个重点最终提交之前一定要编译或跑一次测试。冲突解决后在语法上看着没问题不代表逻辑上没问题。我之前带过一个新人处理完冲突信心满满地推上去结果 CI 直接红了原因是两段代码合并后出现了重复的变量声明编译阶段才暴露出来。代码合并不是把标记删了就算完要保证最终结果是能运行的内容。3.5 rebase 冲突方向感和 merge 完全相反git rebase main过程中如果出现冲突处理思路一样但有一个口碑极差的坑ours和theirs的方向和 merge 是反的。merge 场景下 HEAD里的是你当前分支的内容 feature/login里的是被合进来的另一个分支内容。rebase 场景下则相反你正在git rebase main相当于你希望把自己分支上的提交挪到 main 最新提交之后。此时HEAD指向的是 main 分支的状态theirs指向的反而是你自己分支的提交内容。很多老手在这个场景下都会犹豫一下更不用说新人了。因此你看到 rebase 冲突时不要惯性思维说“左边一定是我的”要先看一眼git status它会在括号里告诉你当前 rebase 进行到第几步。rebase 冲突解决完之后的提交命令也不同不能用git commit来结束而是git add 冲突文件 git rebase --continue如果你在 rebase 过程中实在处理不下去想反悔回到原先状态任何时候都可以执行git rebase --abort它会干净利落地把你拉回执行 rebase 之前的状态所有未完成的改动都不会生效。这是新人处理 rebase 冲突最重要的保命命令。4. 关于 HEAD 的误解连环撞新人的崩溃通常不止一次4.1 “HEAD 是不是某个人”“删除 HEAD 行是不是就好了”崩溃过一次之后新人会开始搜索 HEAD 是什么意思然后就出现各种让人哭笑不得的误解。有人以为 HEAD 是团队里某个同事的英文名处理冲突时特意发微信问“你在这个文件里写的 HEAD 我怎么看不懂”还有新人以为 HEAD这个箭头是“让我在这里补充”的意思在保留代码时把那行箭头一起留在了文件里结果编译器直接报错整个文件变成了无法解析的状态。要记住一条铁律冲突标记本身是给人和 Git 看的分隔符不是代码最终提交前必须全部删除。任何以、、开头的标记行整个删掉。删掉哪个都不影响两边的代码内容它们只是“包装纸”。4.2 Detached HEAD我的手怎么麻了还有一次崩溃来源于git checkout一个提交哈希之后出现的提示You are in detached HEAD state。正常情况下HEAD 指向分支而分支又指向某个提交所以 HEAD 间接指向一个“具名的提交位置”。当你直接检出了一个没有分支名的具体提交比如git checkout 8f3a2b1时HEAD 就变成“悬空”的它不再属于任何分支。你继续在这里改代码、提交提交内容不会记录到任何分支里一旦你切换回其他分支这些提交就会变成“孤儿”很难再被找到。处理办法很简单如果你只是查看历史代码看完切回分支就行不要在这个状态下改东西如果你想基于这个提交开辟新工作执行git branch fix-branch先给它起个名或者直接git switch -c fix-branch这样 HEAD 就重新回到一个具名的分支上了不会再“悬浮”。4.3 reset 翻车了还能救吗reflog 是后悔药比 detached HEAD 更刺激的误操作是git reset --hard。新人经常在搜索“怎么撤销本地改动”时查到这条命令一执行工作区、暂存区、HEAD 全部被重置结果发现自己辛苦写了两天的代码瞬间消失直接原地崩溃。好消息是绝大多数情况下代码仍可以找回来。Git 有.git目录里的“操作日记”机制叫 reflog它会记录每个提交和分支引用最近的变化轨迹。执行git reflog你会看到一行行历史记录每一行都有关键的HEAD{1}、HEAD{2}这样的编号以及对应的提交哈希。找到被你 reset 掉之前的那一条然后git reset --hard HEAD{1}就能回到你执行那个破坏性操作之前的状态代码基本原样恢复。我身边的真实案例里用 reflog 挽回的代码比想象中多得多。当然不要因此而有恃无恐地到处reset --hardreflog 也有过期清理机制这个命令只能算“短时间内有效的后悔药”。4.4 别动不动全盘否定自己的分支很多新人在学会git reset --hard之后遇到任何问题第一反应都是“把我的代码重置回某个干净的提交”。这个习惯很差因为reset --hard是强制重置工作区里没提交的改动会直接丢失暂存区里没提交的改动也会丢而且是真丢不经过确认框没有回收站没有任何“你是否确定”的提示。我建议新人在使用任何带--hard的命令之前先在心里把这个操作翻译成“我准备无视此刻磁盘上所有未提交的东西并强行回到历史某个时间点。”如果这句话让你觉得危险那就不要用。日常想撤销“还没执行 commit 的某个文件的改动”时用git checkout -- 文件名或者git restore 文件名就足够了。5. 让冲突变少的日常习惯以及一个亲测有效的冷静流程5.1 小步提交频繁拉取冲突的本质是“两边从同一个基线各自发生了较长时间的变化”。分支存在时间越长分叉距离越大在同一个区域做修改的概率就越高冲突概率也会指数上升。减少冲突最有效的一招就是小步提交频繁拉取远端最新代码。不要等一个功能写完再 commit写一部分、能编译、自测通过就提交一个。每完成一个小节点就git pull或按照团队规范用git pull --rebase一次。这样你和同事的改动随时都在一个相对接近的基线上绝大多数合并都是毫无波澜的 fast-forward 或自动合并。5.2 少动公共文件先沟通再动手公共文件指的是大家都依赖的、容易同时修改的内容工具函数库、全局配置文件、接口定义、实体模型等。如果你要动的是这样的文件动手前在群里说一句或者先跟最近几周改过这个文件的人打声招呼比闷头改完再让 Git 来判断高到不知道哪里去了。还有一个隐藏冲突来源是代码格式化。你用的编辑器自动格式化可能用得是 2 空格团队标准是 4 空格你随手一保存整个文件几百行的缩进全变了拉下来一合并Git 甚至可能判定两边改动了大部分行冲突面积巨大但实际上核心逻辑改动只有一行。解决方案是团队统一代码格式插件和配置文件并且确保提交中没有掺入无意义的格式改动。5.3 冲突发生时我建议新人的“冷静三步”真遇到冲突时除了第三章的操作流程我补一个心态层面的方法我带过的几个新人用下来反馈都还不错。第一步先离开编辑器手从键盘上拿开看一眼git status知悉有哪些文件冲突然后告诉自己这不是事故这是正常流程几乎所有用 Git 协作的团队都会遇到跟“我犯了大错”无关。第二步告诉自己接下来要做的事情只有一件把冲突文件里那些、、删掉保留需要的代码。整个过程最长不会超过半小时就算真改错了也还有git reflog兜底不存在“无法挽回”这种说法。第三步按冲突文件逐个解决每解决完一个文件就git add那个文件不要一次性处理五个文件再统一 add。这样中间如果编译报错你至少知道错误来自最近处理的那一个文件排查范围很小。5.4 一条冲突处理流程速查表把前面的内容压成一张表方便你贴在工位上场景冲突出现时的“左侧内容”解决后使用命令git merge/git pull当前分支oursgit addgit commitgit rebase mainmain 分支基线右侧才是自己分支git addgit rebase --continuegit cherry-pick当前分支git addgit cherry-pick --continuegit stash pop工作区已有内容删标记后直接继续改即可其中 rebase 那一行的方向问题最反直觉我再来一遍你执行git rebase main是想把自己分支的提交挪到 main 的最新后面此时所谓当前分支其实是 main而你被 reflog 重放的那些提交在 Git 看来反而属于别人的修改。这就是为什么很多老手在处理 rebase 冲突时也经常要打开另一个终端看一眼git status。5.5 最后的个人体会我自己刚入行那会儿第一次看到 HEAD也差不多是那副崩溃状态。后来带新人我发现最好的办法根本不是让他们背 Git 命令而是让他们记住一个原则Git 遇到冲突时不会破坏你的数据它只是需要你做一次小型的代码评审。你平时怎么 review 别人的代码处理冲突时就怎么做把两边的内容都当成候选人你来拍板录用谁。每次解决完冲突提交之前我都会多花两分钟读一遍自己留下的最终版本确认两边的逻辑都完整地融进来了哪怕语法和变量名有瑕疵也宁可在后续提交里再修也不要为了“快点合上”而草率丢代码。这种习惯我用了很多年后来处理再大的冲突也没有真正翻过车。如果你下次又看到那排箭头记住那只是一份代码在等你做决定而你已经知道怎么做了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询