ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Git 冲突标记详解:从惊慌到从容处理 merge 与 rebase

Git 冲突标记详解:从惊慌到从容处理 merge 与 rebase 看到 HEAD的那一刻新人程序员崩溃了。这句形容一点不夸张。我第一次遇到这个符号时脑子里全是“完了代码被覆盖了”“这个文件是不是被 Git 玩坏了”“我刚才 merge 的时候是不是做错了什么”。结果在群里问了一圈老同事只回了一句“这是冲突你把该留的留下该删的删了就行。”后来我带过不少新人几乎每个人都会在第一次遇到 Git 冲突时经历同样的慌乱。这篇内容就想把这件“看着吓人、其实有固定套路”的事讲透冲突标记里的每一行到底什么意思操作层面怎么一步步处理干净哪些命令和工具能帮你更省力以及平时怎么写代码能少踩这种坑。无论你是刚入职不到一年的应届生还是被同事拉去 review 代码的初级工程师看完应该都能在下次面对满屏尖括号倒三角符号时先深呼吸再淡定地把它修完。1. 好先别慌错误信息到底在说什么1.1 你真正看到的那几段标志长什么样Git 冲突的第一现场一般长这样。你用编辑器打开某个文件发现平时干干净净的代码里忽然插入了下面这些行 HEAD let price 100; // 价格调整后 let price 80; // 折扣价支付前生效 feature/discount眼力好的人已经看出来了这段结构其实就是把两段内容拼在一起中间用几条特殊前缀分隔开。很多人一看到 HEAD就以为“HEAD 是什么病毒”“文件被截断了”其实远没有那么恐怖。Git 在用这种方式告诉你当前仓库里存在两个版本的改动它自己判断不了该留哪一份只能把两份都摆出来让你来拍板。这四行标记按顺序拆开各个含义是这样的 HEAD表示从这里开始到下方分隔线为止属于“当前分支”上的内容。是纯分隔线把“当前分支版本”和“要被合并进来的版本”分开。 feature/discount表示右侧内容来自feature/discount这个分支。如果你在中间还看到普通代码那说明那些代码没有冲突只是上下文。也就是说一份冲突文件本质上是一个“待审阅的合并中间态”。Git 并不是覆盖了你写的代码也不是把你提交弄丢了它只是把两边发生矛盾的片段都保留在同一个文件里等你手动删掉多余的标记和版本。1.2 谁和谁在打架HEAD 与合并进来的分支很多新人第一次处理冲突时最大困惑是“到底保留上面这块还是保留下面这块”。这背后有一个关键概念那就是 HEAD。HEAD在 Git 里是一个“指针”它通常指向你当前所在分支的最新一次提交。也就是说 HEAD上面那块内容代表的是“你现在所在分支”的版本。举个例子。你在main分支上然后执行了git merge feature/discount此时若发生冲突HEAD侧就是main分支上的代码而 feature/discount下面的内容就是被合并分支feature/discount的代码。但如果你不是 merge而是在 feature 分支上执行了git rebase main情况会反过来此时“ HEAD ”的内容是被变基分支上的提交而“ 被合并进来的分支 ”实际上是你要“移植”的目标分支。这确实容易绕晕所以我后文在工具章节会专门提醒不要盲目记“上面是我下面是他”一定要先看当前在做什么操作、看一下git status的输出。还要注意一点HEAD并不是永远指向分支名。如果你处于git cherry-pick或解决冲突的特殊状态Git 还会创建一些临时引用文件。对于刚接触 Git 的人来说初期不必深入这些底层结构只需要知道一件事看到HEAD字样就用“当前所在分支版本”去理解大方向不会错。1.3 冲突不是“覆盖”是“合并到一半等你做决定”我再把心态层面的东西多说几句。为什么新人一看到这段标记就崩溃核心是把它等同于“异常”“故障”“程序出错了”。但你应该换一个角度每次正常提交都是一次对新状态的保存而合并过程就是一次需要你确认的“三方会谈”。我习惯用一个生活化类比你和你同事同时负责一份活动方案 Word 文档。你负责改封面标题他负责改报价部分。最后你们各自保存了一份版本想合并到同一个文档里。封面和报价没有冲突系统可以自动合并但如果你俩都改了同一个地方的“活动时间”一个写 6 月 1 日一个写 6 月 15 日系统没法判断哪个是真实意图只能把两行都标红让你来定。Git 的冲突标记就是那两行标红。因此看到冲突标记之后你的正确动作是先把自己从“我怎么这么倒霉”的情绪里抽出来进入“我来评判谁对、谁要保留”的任务模式。Git 怕的不是冲突怕的是你无脑删掉一边或者把包含标记的文件直接提交上去。前者丢失代码后者污染仓库这两件事才是真正需要警惕的。2. 冲突为什么会出现聊透 Git 的三方合并机制2.1 “三方合并”比你想的简单想要真正不慌只知道“冲突 两边改了同一处”还不够。我建议你花三分钟理解 Git 合并的内部判断方式也就是所谓的“三方合并”。Git 在合并两个分支时并不会只拿两个分支的最新状态来对比。它会先找到一个“共同祖先提交”merge base也就是两个分支在分叉前共同基于的那一次提交。然后 Git 会把三份内容拿出来比共同祖先版本base。当前分支版本ours。要合并进来的版本theirs。比较结果大致有三种场景Git 的处理方式只有当前分支改了某行另一个分支没动自动采用当前分支的改动只有被合并分支改了某行当前分支没动自动采用被合并分支的改动两个分支都改了同一处、且改动不一致产生冲突插入冲突标记等你决定为什么算法要找一个“祖先”而不是直接比对两个分支因为如果不看祖先Git 就不知道某一行到底是“ A 分支新增的”还是“ B 分支本来就有的”。看共同祖先才能判断这份改动属于哪一方。这也是为什么 Git 合并通常惊人地聪明两边各自新增不同文件时几乎零冲突只有当两个开发者在同一区域动手脚才会让算法“犯难”。2.2 不只 merge会引发冲突的操作清单很多新人以为只有git merge会产生冲突。实际开发里下面这些操作都可能让你与 HEAD狭路相逢触发操作出现时机关键命令git merge合并分支时双方改动同一区域git merge --abort可放弃git pull拉取远程分支并自动 merge 到本地可临时用git merge --abort回滚git rebase把当前分支提交“重放”到目标分支上时git rebase --abort可放弃git cherry-pick把某个提交摘到当前分支时git cherry-pick --abort可放弃git stash pop/apply把暂存的修改恢复出来时如果冲突可手动解决或继续 stashgit revert回滚一次旧提交时如果附近代码已大变git revert --abort可放弃出现概率最高的是git pull。很多新人没有清晰区分“拉取”和“合并”以为git pull只是把远程代码下载下来、安全无比。实际上git pull等价于git fetch加git merge。一旦你本地有未推送的提交而远程也有别人提交的改动两者如果改了同一个文件同一行Git 就会立刻中止自动合并并在工作区留下冲突状态。如果你在上述操作中途出现冲突又不想当下处理请务必记住对应操作的--abort参数。你可以在冲突状态下执行git merge --abort或git rebase --abort让仓库回到执行该操作之前的状态。这个“后悔药”在搞不清状况时非常救命但也要注意一旦你已经手动改了文件并执行了git add再 abort 可能就不会完整还原了。后面我会再详细说。2.3 冲突能提前预测吗什么时候最容易被缠上虽然不能 100% 预测冲突但冲突确实有高发场景。如果你在下面这些环境下开发请提前做好心理准备分支存活时间过长比如一个 feature 分支开了两三个月才合回主分支。主分支早被其他人推着前进了很远两边改动区域极容易重叠。格式化工具不统一团队里有人用 2 空格缩进有人用 4 空格有人保存时自动把整个文件格式化一遍。哪怕只改了一行功能代码diff 却会把整个文件几百行都标成改动。多人同时改同一个公共配置比如package.json、路由表、数据库配置文件、接口定义文件。这类文件是“兵家必争之地”。大规模重构接近尾声你正把老接口调用改成新接口同事也在同一批文件里做类似替换。冲突并不是“坏事”的同义词。它更像是一个提醒信号你和同事的修改在语义上交织在一起了。提前知道这些高发场景至少能在执行git pull前先把本地改动提交或暂存而不是让现场变得更混乱。3. 从零到一完整跑一遍“制造冲突—解决冲突—提交收尾”3.1 开工先把仓库情况复现出来理论说多了容易飘下面我用一个最简可复现的例子带你完整体验从制造冲突到收尾的完整流程。你完全可以照着在自己的终端里敲一遍。先在临时目录里建一个仓库提交一个初始文件mkdir demo-conflict cd demo-conflict git init # 如果本机之前没有配置过 user.name 和 user.emailGit 会拒绝提交 git config user.name demo git config user.email demoexample.com printf let price 100;\n cart.js git add cart.js git commit -m init: 初始化购物车价格此时仓库里的cart.js只有一行内容let price 100;。接下来基于这个提交创建一个 feature 分支git switch -c feature/discount在feature/discount分支上把那一行改成折扣价版本// cart.js let price 80; // 支付前打八折保存并提交git add cart.js git commit -m feat: 给购物车价格加折扣逻辑然后切回main分支把同一行改成另一个版本模拟同事在主分支上的改动git switch main在cart.js里改成// cart.js let price 100; // 价格评审通过保持不变提交git add cart.js git commit -m chore: 更新价格注释现在两边都在最初那行代码上做了修改而改动内容不一致。当我们把 feature 分支合并回 main 时冲突就必然出现。3.2 冲突发生时的现场长这样执行合并命令git merge feature/discount终端大概率会输出类似这样的信息Auto-merging cart.js CONFLICT (content): Merge conflict in cart.js Automatic merge failed; fix conflicts and then commit the result.注意Automatic merge failed这句英文会让很多新人当场血压升高。这里我要翻译一下它不是“你的仓库坏了”而是“自动合并失败了请你手动解决后提交”。此时第一件事是看状态而不是急着打开文件乱改git status输出中会有一段关键内容Unmerged paths: (use git add file... to mark resolution) both modified: cart.js这表示cart.js处于“双方都修改过”的未合并状态。打开文件看看 HEAD let price 100; // 价格评审通过保持不变 let price 80; // 支付前打八折 feature/discount整个画面是不是和第一节看到的一样现在你知道HEAD侧是main分支的版本feature/discount侧是被合并进来分支的版本。3.3 动手解决三种处理策略解决冲突本身没有任何神奇技巧核心就是“编辑文件去掉标记保留最终想要的代码”。但具体保留哪边需要根据业务意图做判断。这里有三种典型策略我分别演示。**策略一保留当前分支HEAD 侧的版本。**如果你讨论后认为 main 分支的逻辑才是最终方案那就删除及其下面所有内容同时删除标记行只留下// cart.js let price 100; // 价格评审通过保持不变**策略二保留被合并分支feature/discount 侧的版本。**如果你确实想上线这个折扣逻辑那就删除 HEAD 侧内容与标记行只留下// cart.js let price 80; // 支付前打八折**策略三两者结合产出新代码。**这是实际开发中最常见也最考验人的情况。比如两个人并非真的在抢同一行而是各自表达了不同需求新的需求可能是“白天用原价促销期用折扣价”。那么最终代码可以手写成// cart.js const isPromotion true; const basePrice 100; let price isPromotion ? 80 : basePrice;这个策略的前提是你理解了双方意图而不是把两边无脑拼接。如果只是把两段代码都留下来很有可能留下逻辑重复或语法错误。无论采用哪种策略清理完冲突标记后最关键的一步就是保存文件并把文件标记为“已解决”。Git 自身并不知道你手动改了什么它要求你明确告诉它“这个文件处理完了”git add cart.js这一步做完后再次执行git status你会看到之前红色的both modified状态消失Git 提示所有冲突都已解决可以提交了。3.4 提交收尾与回归验证解决冲突后你要做一次正式的提交来收尾。上面git add只是把文件标记为已解决真正的合并结果还要通过 commit 记录下来git commit --no-edit这里我用--no-edit是想告诉你一个细节Git 会为我们生成默认的 merge 提交信息内容类似 “Merge branch feature/discount”或“Merge remote-tracking branch...”。如果你不加--no-editGit 会弹出一个文本编辑器让你确认信息很多新手被卡在这个编辑器里不知道怎么退出。用--no-edit就能避免这个尴尬直接采用默认信息提交。提交后用git log看一下合并结果git log --oneline --graph -3你会看到合并提交以及两个分支的历史被连接起来。到这里一个完整的冲突处理流程就走完了。但先别急着推送。我曾见过新人处理完冲突就立刻git push结果 CI 直接报语法错误。聪明的做法是把全局测试跑一遍或者至少编译一下涉及的文件。如果这是前端项目跑一遍 lint 和单测如果是后端项目把相关模块的测试跑通。确认没有引入运行问题后再推远程才算真正闭环。4. 不想一行行删标记试试这些工具和命令4.1 一句话搞定的 Git 自带命令ours 和 theirs 的正确用法如果你判断整个文件的冲突并不需要“手工融合”直接采用其中一侧即可那就没必要在编辑器里逐个删标记。Git 有两个常用命令能直接把文件恢复到某一侧的完整版本。在git merge冲突状态下执行下面的命令表示“我要当前分支HEAD 侧这个文件的完整版本”git checkout --ours -- cart.js git add cart.js如果想要“被合并分支”的完整版本则改用--theirsgit checkout --theirs -- cart.js git add cart.js如果你用的 Git 版本较新官方更推荐用新式命令git restore效果是一致的git restore --ours -- cart.js git add cart.js我必须强烈提醒你一个坑ours和theirs的含义取决于你正在执行的操作不是永远不变的“当前分支 ours”。在普通git merge中ours是当前分支theirs是合入的分支但在git rebase场景中两者会调换位置。因为在 rebase 时Git 会把你的提交逐个“重放到”目标分支上面此时你的提交反而被当作“theirs”。如果你不确认场景就乱用这个命令很可能把文件恢复到错误的一侧心情雪上加霜。所以在使用--ours/--theirs前一定先执行git status看清楚当前是在 merge 还是 rebase操作对象是什么分支。信息不完整时用编辑器打开文件观察 HEAD和后面的分支名比自己瞎猜可靠得多。4.2 可视化解冲突编辑器与三方合并工具怎么配对新人来说纯文本手改冲突虽然能锻炼基本功但效率确实低了点。Visual Studio Code 等主流编辑器对冲突提供了非常友好的可视化支持。用 VS Code 打开冲突文件你会在冲突区域上方看到几个按钮Accept Current Change接受当前分支版本对应 HEAD 侧。Accept Incoming Change接受传入分支版本对应另一边。Accept Both Changes两段都保留。Compare Changes并排对比两段内容。这三个按钮很直观但也要注意Accept Both Changes 常常导致代码重复定义或逻辑重复使用前要想想这样合是否合理。VS Code 新版还内置了三方合并编辑器在源代码管理面板的“合并更改”里可以直接看到 base、ours、theirs 三列处理复杂冲突时比单纯的文本模式清晰得多。如果你更习惯用 JetBrains 家的 IDE比如 IntelliJ IDEA 或 WebStorm那处理冲突的体验会更好。它的冲突对话框把左侧当前版本、右侧传入版本、中间合并结果放在同一屏你点箭头就能把代码块送到中间区域还有高亮显示帮助你判断每一段差异。如果你是一个 CLI 爱好者也可以配置git mergetool让它调起图形化合并工具git config --global merge.tool vscode git config --global mergetool.vscode.cmd code --wait $MERGED git config --global mergetool.keepBackup false配置好后冲突发生时执行git mergetoolGit 会逐个打开有冲突的文件等你在外部工具里处理完并保存关闭后再自动把文件标记为已解决。上面把mergetool.keepBackup设为 false是为了防止 Git 在处理完冲突后留下一堆cart.js.orig备份文件那种文件留在项目里除了碍眼没什么用。4.3 rebase 场景下的“方向感”问题前面两次提到 rebase 下ours/theirs方向变化。这里展开讲讲因为它真的太容易坑新人了。假设你在 branch-a 上写了两笔提交现在想让这两笔提交“搬家”到 main 分支的最新提交上面git switch branch-a git rebase main执行过程中如果某一次提交与 main 冲突Git 会停下来。此时 HEAD表示的是谁呢rebase实际上做的事情是把 branch-a 上的提交一个个摘下来依次“贴”到 main 的顶端。在贴第一个提交的瞬间Git 的“当前 HEAD ”已经是 main 分支的最新提交了而不是你 branch-a 上的原代码。至于 branch-a 上被重放的那笔提交反而是被临时当作“传入”对象。所以在这一类冲突里HEAD侧 main 最新版本。 ...侧 你要搬过来的 branch-a 提交。这跟普通 merge 的理解习惯完全相反。如果你的最终目的是让 branch-a 的改动在保留的基础上兼容 main 的变化那么遇到冲突时通常你需要保留两边内容中合理的那部分。也就是说不能简单用“选择 HEAD 侧”来解决问题。而且在 rebase 过程中如果 branch-a 有多笔提交都改了同一处地方你可能要连续解决好几次同样的冲突。遇到这种情况我的建议是如果冲突复杂度高可以考虑放弃 rebase改用 merge。git rebase --abort可以安全退出当前变基操作然后走 merge 流程只解决一次冲突。对于新人而言追求“代码历史线性好看”远不如“把功能安全地合并回去”重要等经验积累够了再逐步使用 rebase 也不迟。5. 新人高频翻车点与避坑实录5.1 高频问题速查表实践中最常见的问题我用表格帮你整理在这里遇到现场情况可以快速对照。现象最可能的原因正确的做法git pull后满屏 HEAD远程分支与本地未推送提交改动了同一区域解决冲突后git add、git commit --no-edit不想解决就git merge --abort冲突文件里只有却没有文件被误删或编辑过程异常中断用git status查状态必要时git checkout --ours恢复一侧再处理手工删完标记但git status仍显示 Unmerged忘记执行git add标记已解决执行git add file提交时弹出一个黑色全屏编辑器退出不了Git 默认打开 vim 等编辑器让你输入提交信息输入:wq保存退出或下次直接git commit --no-edit我改完冲突但被别人先 push 了本地基于旧提交做的合并在 main 上再执行git pull小规模冲突再次解决即可我用git merge --ours想保留本地结果东西不对对ours方向理解错了先git status确认场景或打开文件看HEAD和分支名合并后出现了很多.orig文件git mergetool默认保留了备份文件配置mergetool.keepBackup false后删除多余.orig文件5.2 最危险的三个操作错提冲突、乱 abort、标记已提交第一件危险操作是把还带着“尖括号”的冲突文件提交上去。你可能会觉得奇怪谁会提交这种东西但实际上 Git 并不会阻止你提交带有冲突标记的文件。只要你在冲突状态下执行了git add和git commit哪怕文件里还残留 HEAD提交也会成功。代码编译大概率直接失败。怎么知道自己是否中招搜索当前仓库里是否存在冲突标记可以执行git grep -n -E ^(|)如果搜到结果立刻打开对应文件把标记清理干净再git add、git commit。如果这个错误提交已经被推送到远程不要试图通过git push --force强行覆盖协作分支正确方式是再补一个修复提交把隐患告诉队友。第二件危险操作是乱执行--abort。很多人一看到冲突提示就开始慌不管三七二十一执行git merge --abort。如果你还没手动改文件abort 是安全且有效的。但如果你已经花了不少时间手动编辑冲突文件或者已经执行过git addabort 会把当前未完成的合并状态撤销你刚才的修改很可能会丢失。因此--abort是“后悔药”而非“止痛药”。真想放弃请确认自己没有任何需要保留的临时改动再执行 abort。第三件危险操作是“只解决一个文件就提交忽略仓库中其他冲突文件”。一次git merge可能涉及多个文件同时冲突。不要在一个文件处理完后立刻 commit。正确流程是打开git status把所有both modified的文件逐个解决然后逐个git add最后统一提交。漏掉某个文件会让仓库处于半合并状态后续操作越来越混乱。5.3 排查冲突的固定套路新人处理冲突时容易像无头苍蝇。我建议你把下面这组动作背下来形成肌肉记忆第一步先执行git status搞清楚自己在哪个分支、正处于什么操作状态、哪些文件处于未合并状态。第二步打开一个冲突文件通读整个冲突区域确认两侧代码各自代表什么含义如果冲突区域面积很大先用肉眼或编辑器快捷键跳到下一个标记逐个解决不遗漏。第三步针对每个冲突区域判断采用左侧、右侧还是手工融合然后删除多余标记。第四步保存文件git add标记为已解决。第五步全部文件处理完后git status确认没有冲突残留跑一遍测试最后再 commit。这套流程看起来普通但能救很多人的命。它最大的优点是把“处理冲突”从情绪发作变成机械流程你不太会再因为漏改一个文件而返工。6. 冲突少一点效率高一点日常开发中的六个习惯6.1 提交要小同步要勤处理冲突最有效的办法不是练就一身高超的合并技巧而是从一开始就不给冲突生长的土壤。第一个能长期奏效的习惯是保持提交颗粒度小、同步频率高。小提交意味着每次改动聚焦在一个明确目标上比如“新增注册接口”“修复首页轮播闪屏”“调整登录页按钮文案”。当分支合并时Git 面对的是相对清晰的小 diff冲突定位起来也容易。如果一整天把所有改动攒成一个大提交里面既有重构、又有修 bug、还有文案调整一旦冲突出现你根本看不出哪部分和同事的改动重叠解决起来如同大海捞针。同时本地开发尽量不要长期不拉取远程。早上开工前拉一次git pull午休前再拉一次下班前如果代码没提交至少先git stash或提交到临时分支保持工作区清爽。每次同步的间隔越短本地分支与远程分支的差异越小合入时产生冲突的概率也就越低。6.2 分支短命、任务聚焦第二个习惯和分支管理有关。团队协作时我强烈建议“分支不要活太久”。一个 feature 分支从创建到合并回主干理想时间是几天内最长不要超过一两周。为什么分支存在时间越长它和主干之间积累的差异越大合入时面临冲突的风险也越大而且一旦主干上新改动了公共模块长分支上的代码很容易变成“历史遗留问题”你需要一次性吸收大量外部变更。如果你的任务确实很长那就拆成多个小任务每个小任务独立开分支、独立合入。比如做一个大型用户中心重构可以先合并“用户表结构调整”再合并“用户查询接口重写”最后合并“前端页面迁移”。每步都在主干上及时集成和验证冲突就能被限制在小范围里。这里要说明这个建议适用于大多数普通业务团队。如果你的团队有专门的分支模型要求比如需要长期维护 release 分支那么请以团队已有规范为准但“减小单次合并范围”的原则依然通用。6.3 “共同区域”提前打招呼格式化规则先统一第三个习惯是人为干预。很多冲突集中出现在公共文件上比如路由表、接口定义、依赖清单、配置文件。如果你准备动这些区域而在git log里看到其他同事最近也提交过这些文件最好通过即时通讯工具或在周会上提前说一声“我下午要改路由表可能动到 /api/v1 这一段你们有在改的话告诉我。”这种沟通成本极低却能把不可避免的冲突变成可控的排队。另外请务必统一团队的格式化规则。一个我见过无数次、也处理过无数次的场景是团队里没有 eslint/prettier 统一配置有人保存文件时自动格式化导致 200 行只用改 1 行提交 diff 里却有 150 行格式变化。这直接导致合并冲突概率飙升。正确的做法是把 prettier/eslint 配置提交到仓库根目录编辑器设置成保存时按同一套规则格式化并且做一条硬性约定不要在功能改动文件里夹带大规模格式调整。格式化用单独提交别和功能代码混在一起这会让 review 和后续合并都轻松很多。第四个习惯是“减少无必要的重新排序和大范围重命名”。变量重命名、文件移动、目录结构调整都属于高冲突操作因为它们会触碰大量行。必要时可以独立成一次提交并及时推送但不要在你个人分支上做一大堆重构捂了很久不推。第五个习惯是积极使用git fetch而不是每次直接git pull先看看远程发生了什么变化再决定如何合入。第六个习惯则是解决冲突后一定要看到“最终代码合理”而不是“只要 Git 不再抱怨就行”。说实话看到冲突标记的第一反应我以前也恐慌过。现在每次看到 HEAD我倒觉得它像一位认真负责的同事拿着两份方案在我面前问“这两处改动我拿不准你来定。”你要做的不是把它当成系统故障而是把它当成一次代码评审的起点。把上面这些流程和习惯练熟以后再遇到满屏尖括号你就不会崩溃了反而能冷静地和同事确认“这个冲突我来解改动区域我都看到了等一下我推给你 review。”
返回列表