
刚学 Git 的时候我最怕的就是“拉取”和“合并”。那时候分不清 pull 和 fetch 的区别也不知道 merge 和 rebase 到底有什么不同总是一顿操作猛如虎最后发现本地分支被远程一堆新提交碾过去自己写的东西夹在冲突标记里不知道怎么办。后来在实际项目里被队友带着排查了几次才算把这套流程彻底理清楚。这篇就聊聊 git 拉取和合并讲明白它们到底在干什么、什么时候用哪种姿势、遇到冲突怎么不慌。“git 拉取和合并”是每个用 Git 的开发者几乎每天都会碰到的操作它决定了你怎么把别人的代码同步到本地又怎么把你自己的改动安全地并回分支。这篇文章不是为了背命令而是从工作原理讲到真实操作再给出一套可以直接上手的冲突解决思路。不管你是刚接触 Git 的初学者还是已经用了一段时间但一直没系统梳理过的朋友这篇应该都能帮上忙。1. 拉取和合并的底层逻辑先搞懂 Git 是怎么存分支的1.1 本地分支与远程跟踪分支的关系要理解拉取和合并先得知道 Git 里的分支到底是什么。很多新手以为分支是“分出来的一个文件夹”其实不对。Git 的分支只是一个指针指向某一次提交。你每次提交分支指针就会往前进一位。当你执行git clone或者第一次git remote add之后Git 会为远程仓库创建一个对应的“远程跟踪分支”名字一般是origin/main。这个分支不是给你直接操作用的它只负责记录“上一次你从远程仓库看到的版本是什么”。你本地main分支该怎么前进完全是你自己提交决定的事而origin/main怎么前进只有在你主动和远程服务器通信时才会更新。看不看得见本地和远程的差距直接决定你后面拉取和合并的选择。比如你本地提交了几次而同事也往远程推了新代码这时候你的main和origin/main就会分叉。用git log --oneline --graph --all能看到类似这样的结构* 6a3b2c7 (HEAD - main) feat: 完成登录模块 | * 281d9e3 (origin/main) fix: 修复空指针异常 |/ * f12ab30 基础框架搭建这种情况下你本地比远程多一个提交远程比你本地多一个提交分支变成了一条岔路。接下来的拉取和合并就是要让这两条路重新接在一起。1.2 git pull 其实就是 fetch merge很多教程会直接告诉你“git pull 是拉取代码”但这个说法太笼统了。Git 官方文档里说得非常清楚git pull实际上执行的是两个动作先git fetch再git merge。git fetch负责把远程仓库最新提交下载到本地更新origin/main这个远程跟踪分支。注意它不会碰你的工作区也不会碰你本地分支所以是绝对安全的操作。git merge则负责把你下载下来的新提交合并到你当前所在的分支上。所以当你执行一条git pullGit 内部大概走了这么两步git fetch origin git merge origin/main理解了这一点你就能明白为什么git pull有时候会直接快进更新本地分支有时候却弹出一个“Merge branch origin/main”的合并提交。那是因为两边分叉了git merge自动创建了一个合并提交。很多人看到这种提交一脸懵其实是正常现象。先弄懂这条链是掌握拉取合并最关键的一步。你不需要记住所有命令只要脑子里知道“拉取”不是一个原子操作它是“先拿回来”和“再合并”的组合后面很多疑难杂症都能自己想明白。1.3 合并、变基与快进三个概念一次理清既然拉取的核心是合并那合并里最基础的概念就绕不开快进合并、普通合并、变基。快进合并fast-forward当你在本地没有新提交只是远程前进了Git 就能把你本地的分支指针直接往前挪挪到远程最新提交的位置。因为整条提交历史是一条直线不需要产生新的提交。普通合并merge当本地和远程都各自有了新提交Git 会找到两个分支的共同祖先然后把两边的改动都整合成一个新的“合并提交”。这个提交有两条父提交用来记录合并来自哪两个分支。变基rebase它不是直接合并而是把你本地的提交先“摘下来”然后以远程分支最新的提交为基础再把你的提交一个个重新提交上去。最终的效果是你的提交看起来像是排在远程提交后面的一条直线但提交本身的 hash 会改变。用生活里的场景类比两拨人各自改同一份文档。快进就是只有一拨人改了另一拨人直接拿最新版普通合并是两拨人都改了需要一个助手把两边改动汇总到一份文档里变基则是你把你改的段落复制出来粘贴到对方最新版本后面最后整个文档的修订记录看起来只有一条时间线。这三者的区别很关键用一张表可以看得更清楚方式是否产生额外提交提交历史形态适用场景快进合并不产生线性、简洁本地无分叉直接同步远程普通合并产生合并提交保留分叉信息忠实记录功能分支合并进主干变基产生新提交不保留原 hash线性、最干净本地个人分支同步主分支2. 拉取代码的几种姿势pull、fetch 和 pull --rebase 怎么选2.1 git pull最直接的同步操作但它有自己的脾气git pull是大多数人最常用的命令操作上确实方便。什么都不加在本地主干分支上直接就能把远程代码拉下来git pull origin main如果当前分支和远程分支没有分叉上面这条命令会快进到远程最新版本你本地工作区立刻变成远程的样子。如果两边有分叉Git 就会自动进行合并并生成一个合并提交。但是这里有几个容易踩坑的点。第一如果远程有新提交而本地也有未提交的改动且两者恰好改了同一个文件的同一行git pull会直接报错告诉你本地修改会被覆盖。这时候你仓库里的文件并不会丢但合并操作会被中止你需要先把本地改动处理掉。第二git pull默认行为是不带任何防护的分叉时它可能直接生成一个你没有预期的合并提交。很多人不想要这种“自动合并提交”于是更愿意用下面的方式。如果你想让git pull更安全一点可以加上--ff-only参数git pull --ff-only origin main它的意思是只有能快进时才拉取如果本地和远程分叉了就报错退出不自动合并。这样就把决策权交还给你而不是让 Git 稀里糊涂地给你合并。2.2 git fetch只下载不合并适合做精细操作git fetch是最“老实”的拉取方式。它会把远程的新提交下载到本地更新origin/main但你的工作区、本地分支、暂存区统统都不动。很多人不习惯先 fetch觉得多一步操作很麻烦。但这其实是官方推荐的做法。你完全可以先把远程的提交拉下来看一圈再决定怎么处理git fetch origin然后你可以对比本地和远程的差距git log --oneline origin/main..main # 查看本地有而远程没有的提交 git log --oneline main..origin/main # 查看远程有而本地没有的提交 git diff main origin/main # 查看两边的内容差异看完之后再决定是git merge origin/main、git rebase origin/main还是干脆按兵不动。这个习惯非常好尤其适合团队合作中需要谨慎合并的场景。你不想在不明所以的情况下一股脑把远程代码合进来把本地工作区搞坏。坦白说我现在已经很少直接用裸git pull了基本都会先 fetch然后心里有数之后再做合并或变基。虽然命令多敲了一条但安全感提升非常多。2.3 git pull --rebase让提交历史保持线性当你本地分支有一些提交同时远程也前进了一些提交这时候git pull默认会帮你 merge产生一个合并提交。如果你不想让本地分支出现太多“分叉再合并”的历史可以用 rebase 方式拉取git pull --rebase origin main这条命令会把你本地还没有推送到远程的提交先放到一边然后把远程最新的提交拉到本地最后再把你的提交逐个重新提交上去。执行完之后你的提交历史是一条直线看起来特别清爽没有多余的合并提交。不过这里有一个非常重要的红线不要对已经推到共享分支上的提交做 rebase。因为 rebase 会改变提交的 hash如果你的提交已经被别人拉走了你再 rebase 一次别人合并时就会出现完全分叉的历史。所以git pull --rebase更适合你本地有私有提交的场景比如你在功能分支上写了半天还没推上去想同步一下主分支的新代码用它是很合适的。2.4 拉取行为的默认配置建议针对git pull到底用 merge 还是 rebaseGit 提供了全局配置git config --global pull.rebase false # 默认 merge git config --global pull.rebase true # 默认 rebase git config --global pull.rebase merges # 只对新提交变基保留合并提交我个人的建议是日常开发中如果只是同步远程主干代码把pull.rebase设置为true会更省心能避免大量无意义的合并提交。但如果你是在做分支合并管理的负责人希望忠实保留每次合并的时间点那还是用merge更符合需求。另外配合上pull.ff配置也可以提高安全性。例如git config --global pull.ff only这样默认情况下只要不能快进git pull就不会自动合并而是提示你手动决定。对于想减少“意外合并提交”的新手这个配置很值得试。3. 合并分支的完整实操从准备到冲突解决3.1 合并前的检查清单真正手动执行git merge的时候不要上来就敲命令。先看一下自己的仓库状态养成核对的习惯会少踩很多坑。第一步确认工作区干净。执行git status如果显示有未提交的改动建议先git stash暂存起来或者干脆提交掉。因为合并时 Git 会尝试把另一个分支的改动应用到当前工作区如果本地有未提交且与合并改动冲突会让合并变得非常复杂。第二步更新远程跟踪分支。在合并之前先git fetch origin确保你要合并进来的远程代码是最新的。如果你直接git merge一个好几天前的origin/main那合并进来之后可能还会面临一大堆新冲突。第三步看历史判断分叉情况。执行git log --oneline --graph --all -10这一步能让你直观看到本地和远程各自领先多少个提交。如果两边没有分叉后面就是纯快进如果分叉了你就知道冲突大概率会发生。第四步可以顺手确认一下你即将合并进来的内容到底改了什么git diff --stat HEAD origin/main这一步很管用尤其是当你怀疑目标分支上有不成熟代码时。提前看一眼改动范围心里有底再合并能避免给别人做“免费测试”。3.2 执行合并快进合并和普通合并的区别做完检查之后就可以正式合并了。假设你在main分支上已经 fetch 过远程接下来执行git merge origin/main如果本地没有新的提交origin/main的提交就排在本地提交的下游Git 会显示类似Fast-forward的提示并直接移动指针这时候不会有额外提交。如果本地有自己的提交而远程也有新提交Git 就会进入普通合并模式。它会在你本地生成一个合并提交并可能需要你填写提交信息。不想进入编辑器的话可以加上--no-editgit merge origin/main --no-edit很多团队在把功能分支合并回主干时会特意使用--no-ff参数git merge feature/xxx --no-ff--no-ff的意思是“禁止快进合并”哪怕能快进也强制生成一个合并提交。这样做的目的是在主干分支上保留一个明确的“功能合并节点”方便将来回溯历史、做回滚。这是团队协作中非常常见的技巧。3.3 冲突的本质与解决流程含详细示例冲突是合并操作里最让新手头大的事情。但冲突的本质没那么神秘两个分支在同一个文件的同一块区域都做了不同的修改Git 不知道到底该听谁的于是把它标记出来让你做决定。一个典型的冲突标记长这样 HEAD 这里是你当前分支的代码 这里是远程分支的代码 origin/main HEAD到之间是你本地分支的内容到 origin/main是要合并进来的分支内容。你需要做的是打开文件读一遍两边代码手动保留正确的内容然后把冲突标记全部删除。完整的操作流程如下首先执行git status查看哪些文件冲突了Git 会列出both modified的文件。打开冲突文件逐个处理把标记和不要的代码删掉。处理完所有冲突后执行git add file把文件标记为“已解决”。全部解决后执行git commit完成这次合并。举个例子假设你在登录模块里加了几个参数远程的同事也在同一个函数里改了逻辑冲突后是这样的 HEAD public User login(String username, String password, String deviceId) { return authService.login(username, password, deviceId); } public User login(String username, String password) { return authService.legacyLogin(username, password); } origin/main如果你确认设备标识deviceId是需要的同时又不希望丢掉远程同事的legacyLogin改动最终正确的代码可能是结合两者public User login(String username, String password, String deviceId) { return authService.login(username, password, deviceId); }然后把所有、、标记删除。处理完之后git add最后git commit。如果纠结到一半发现实在处理不了想回到合并前状态可以用git merge --abort这条命令会立刻停止合并把工作区恢复到合并之前的状态非常实用。3.4 合并后的验证与回滚预案合并提交生成之后并不是万事大吉。至少要做两件事。第一件看合并提交的形态。执行git log --oneline --graph -5确认合并的结构是否符合预期。如果你用的是普通合并会看到一个有两个父提交的合并节点如果是快进合并历史就是一条直线。第二件跑编译、跑测试。合并之后代码能通过语法检查不代表逻辑上没问题。冲突解决时你手动修改过代码很容易引入潜在错误。不验证就推代码是对团队的不负责。如果合并后你发现结果完全不对想撤销这次合并有两个方向合并还没有生成提交时直接用git merge --abort。合并已经提交但还没有推送到远程可以用git reset --hard HEAD{1}回到合并前位置或者去 reflog 里找一个更早的 hash。合并已经推送到远程那就不能随便改了这时候更适合用git revert -m 1 merge-commit生成一个反向提交来抵消这次合并的效果。记住区分“未推送”和“已推送”这是选择回滚方式的关键。4. 常见问题与排查技巧实录4.1 误合并之后如何安全撤销误合并是每个开发者都会遇到的情况。比如你本来想合并一个分支结果手滑合错了或者合并完之后发现代码彻底跑不起来。这时候“撤销”分几种场景。如果你还没有退出合并过程直接git merge --abort如果已经生成了合并提交但没推送最方便的是git reflog配合git reset。执行git reflog你会看到本地分支的所有历史动作包括每一条命令执行后的 commit hash。找到合并前那一条记录比如HEAD{1}然后强制切回去git reset --hard HEAD{1}这里强烈建议先确认reflog里选的位置是对的因为reset --hard会把当前工作区改成目标提交的状态任何没有提交的改动都会被丢弃。如果合并提交已经推送到了公共分支那么不要用reset因为那样会改写远程历史坑到别人。正确做法是反向提交git revert -m 1 merge-commit-hash-m 1表示保留合并提交的第一条父提交线即保留主干上的内容取消合并进来的分支改动。这个过程会生成一个新的提交整个过程对团队其他人是安全的。4.2 如何快速定位所有冲突文件冲突文件一多一个个翻会非常低效。有几个命令可以帮你快速定位。最简单的git status --short冲突文件会显示成UU开头表示两处修改都未合并。想只输出冲突文件可以用git diff --name-only --diff-filterU这条命令会把所有处于“未合并”状态的文件名列出来干净利落。拿到列表后你可以按文件逐个打开处理。如果你用的是现代 IDE编辑器里也会直接列出冲突文件并高亮冲突段落。但我还是建议在命令行先把列表跑出来因为你可能在终端里才能看到最完整的消息。处理完一个文件后记得立刻git add这个文件Git 就会认为它已经解决了。最后再git status确认没有剩余冲突。4.3 本地有未提交修改时拉取会不会丢代码这个问题我在团队里被问过很多次我本地还没提交直接git pull会不会把代码冲掉答案是不会但前提是远程的改动和你的本地改动没有重叠。Git 在处理时如果发现要拉下来的内容会覆盖你未提交的同文件修改它会中止操作报错提示你“本地修改会被覆盖”。这种情况下你的代码还在只是拉取失败了。正确的应急流程是git stash git pull git stash popgit stash会把本地未提交的改动暂时存到一个独立的工作区里然后清空当前改动。拉取完成后git stash pop再把改动恢复出来。如果这时候你的改动和刚拉下来的代码冲突了pop会像 merge 一样产生冲突标记你手动解决后还需要手动执行git stash drop把暂存的记录清理掉。如果你想放弃本地未提交的改动彻底同步成远程版本可以直接git reset --hard origin/main但这条命令会把你所有未提交的修改直接丢掉执行之前一定要想清楚。4.4 远程分支被清理后本地还残留怎么办有时候远程分支已经删除了但本地用git fetch之后仍然能看到那个分支的踪影。这是因为默认情况下git fetch不会自动清理本地对应的远程跟踪分支引用。时间一长你的本地仓库里会堆满一堆“幽灵分支”。解决办法是用--prune参数git fetch --prune或者git remote prune origin它会检查远程仓库里已经不存在了的分支并把本地对应的引用清理掉。如果你用的是 IDE 里的 Git 工具大部分也都支持“Prune Remote Branches”选项。养成定期执行git fetch --prune的习惯能让你本地仓库长期保持干净也避免你盯着一个已经不存在的远程分支发呆。4.5 团队合作里拉取合并的统一约定拉取合并不只是个人习惯的问题还直接影响团队协作效率。我参与过的几个项目里拉取合并做得好的团队都有一个共同点约定清晰且每个人都遵守。我们团队的约定大致是这样的主干分支保持线性开发者在本地功能分支上开发需要同步主干最新代码时统一用git fetch origin加git rebase origin/main避免在本地产生合并提交。功能分支合并回主干时用 --no-ff这样主干上能清晰看到每个功能分支的合并节点方便回溯和回滚。提交粒度要小每次提交尽量只做一件事不要在一个提交里混入格式化、重构、功能代码。粒度越小冲突越少回滚越精准。推送前一定要先拉取并验证先git fetch确认远程没有新提交再推送。如果远程有新提交先 rebase 或 merge 后再推。禁止在共享分支上 reset --hard已经推送的提交一律用git revert回滚不要改写历史。这些约定看起来简单但真正做到位能避免绝大多数拉取合并相关的灾难。我个人在实际操作中的体会是拉取和合并从来不是“敲一条命令”那么简单它考验的是你对 Git 分支模型的理解程度。刚入行时我害怕冲突总希望有工具能自动帮我解决一切踩了几次坑之后才发现冲突反而是理解代码脉络的最好机会。每次处理完冲突我都会顺手看看对方改了什么、为什么改这种习惯让我少写了无数 Bug。最后再分享一个小技巧如果你不想再被git pull的默认行为坑到可以把pull.ff设置成only这样一旦遇到分叉Git 会强迫你停下来思考而不是自作主张帮你生成一个合并提交。拉和合说到底都应该由你自己掌控。