ARTICLE DETAIL

资讯详情

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

git fetch 和 git pull 到底有什么区别?掌握这套工作流不再怕冲突

git fetch 和 git pull 到底有什么区别?掌握这套工作流不再怕冲突 先问一个很基础的问题你平时更新远端代码是习惯敲git pull还是git fetch如果这个问题让你犹豫了几秒那这篇内容值得你读完。很多开发者把这两个命令当成同一件事的两种写法无非觉得 pull 更“彻底”一点可在实际项目里两者的差别足以改变分支历史的走向也足以解释你为什么经常毫无防备地撞上冲突。我见过不少同事每天都在用git pull却不知道 IDE 后台早就替他执行过fetch也见过有人因为一次默认的pull在长期分支上留下了一串看不懂的 merge commit后来花了大半天才整理干净。所以这篇文章我想从头讲清楚这两个命令在实际开发里的分工fetch负责把远端状态搬运到本地pull负责在搬运之后立刻完成合并。文章会先用最直白的方式给出区别再深入 Git 内部讲机制最后给出一套我自己常用的工作流和问题排查清单。如果你是刚学 Git 的新手前面这些内容能帮你建立框架如果你已经写了好几年代码后面几节也能把一些模糊的细节重新梳理一遍。1. 直观区别fetch 只搬运pull 还拆箱1.1 用“快递”打个比方看货和收货是两回事假设你在网上买东西。git fetch相当于快递员把包裹放到驿站你收到通知知道包裹到了、知道它有多大但你没拆开东西还没到你手上git pull则相当于驿站直接帮你把包裹拆开把里面的东西摆到你桌上顺便把包装纸扔进垃圾桶。用术语说fetch只是把远端仓库的提交记录、分支引用同步到本地的一个“暂存区”你的工作区文件一个字符都不会变pull则会在fetch之后立刻把远端的新提交合并进你当前所在的分支并且更新工作区。这就是两个命令最核心的区别fetch是只读操作pull是写操作。这里的“写”不只是改几个文件而是可能改写你本地分支的提交历史结构。你可以随时执行一百次fetch仓库会越来越接近远端的最新状态但你的工作区、当前分支的 commit 历史都不会被破坏最多只是在后台多出一些对象数据。而pull会在某一次之后突然弹出冲突提示或者多出一条 merge commit这才是不好预判的部分。1.2 一张表格看全两者的行为差异如果你更习惯用表格来对比我把双方的行为差异整理成了下面这个样子对比维度git fetchgit pull是否可以随时安全执行是几乎可以随便用否可能影响工作区和历史是否更新工作区文件不更新默认合并后更新是否更新本地当前分支不更新更新是否可能产生额外的 merge commit不会默认 merge 策略下可能是否可能中途报冲突不会可能核心用途查看远端状态、同步远程跟踪分支快速同步并整合本地与远端这个表格基本能回答九成场景下的疑问。剩下的那一成就要看你对 Git 里那些“看不见的分支引用”有没有清晰认识。下面这一节专门讲这个。1.3 随手 pull 埋下的隐性雷很多人在自己的分支上开发每天到办公室的第一件事就是git pull。假设你们都在 main 分支上工作所有人都基于 A 开始开发同事往远端连续推了 B、C 两个提交而你在本地基于 A 又提交了 D。此时你敲git pullGit 会把 B、C 拉下来发现本地 main 的历史是A - D远端 origin/main 的历史是A - B - C两边出现了分叉。如果你是默认配置Git 会在这里创建一个合并提交 M把 D 和 B、C 同时接上。这个 M 不是你主动写的它就因为你敲了一条pull就诞生了。单看一个人的分支好像没什么可如果同一个分支上有三四个人都在这么干那这个分支的提交历史会像拧麻花一样一条线变成两条又汇成一条。等你想从历史里定位一个功能改动时眼前全是自动合并节点谁也看不清这条分支到底改过什么。更麻烦的是如果同事和你恰好改动了同一个文件的同一个区域pull会立刻停下并提示冲突。这时你还没有动手解决工作区里已经全是冲突标记。对刚入门的同学来说这种突如其来的“半成品状态”特别让人手足无措。我用了很多次之后总结出的教训就是pull是一个有副作用的命令使用前最好清楚它会把你的仓库导向什么状态。2. 深一层看机制fetch 和 pull 在 Git 仓库里各自做了什么2.1 先认识三个不同的“位置”要真正理解fetch和pull的差异需要知道一个 Git 仓库里并不是只有一份代码。粗略地说里面至少有三种状态HEAD 指向的当前分支、本地分支引用以及远程跟踪分支。远程跟踪分支通常长这样refs/remotes/origin/main日常命令里你会写成origin/main。它存在于你的本地仓库但不是一个你可以直接工作的分支。它更像是远端仓库在你本地的一个“镜像位”你可以用git log origin/main查看它的提交记录却不能用git checkout origin/main正常切过去切过去会变成 detached HEAD——它只是一个指针记录着“远端 main 分支上次被你看到时的样子”。fetch最核心的事情就是把远端最新提交和分支信息更新到这些refs/remotes/origin/*指针上同时把新下载的对象放进.git/objects。做完这些之后你还会看到一个叫FETCH_HEAD的临时引用记录本次拉取到的提交。多数人不怎么关注FETCH_HEAD但当你看到别人写git merge FETCH_HEAD的时候至少要明白这东西指向什么。有没有注意到一个现象很多 IDE 在你打开项目的瞬间其实已经在后台自动执行过一次fetch了。你感觉什么都没发生但git log origin/main的内容已经悄悄变化。这正是fetch的典型特征——安静、安全、不影响你正在做的事。2.2 pull 不是一条命令是两条命令的组合很多人以为git pull是一个原子操作其实它内部是先执行一次fetch再执行一次合并。如果你用的是默认配置git pull origin main展开后大致等价于git fetch origin main git merge FETCH_HEAD如果你把 Git 配置成 pull 时使用 rebase比如设置pull.rebase true后再执行git pull --rebase origin main它就等价于git fetch origin main git rebase FETCH_HEAD看到没有pull只是把两个动作打包了。所以你要分析pull的副作用本质上就是在分析merge或rebase的副作用。很多人只记住了fetch和pull的区别却忽略了后半段的merge和rebase才是决定历史走向的关键。这也是为什么我觉得与其只比较fetch和pull不如把“拉取数据”和“合并数据”这两件事彻底分开来看。如果你想完全掌控合并方式其实不需要通过修改pull的默认行为来实现。直接先fetch再手动执行merge或rebase命令更透明仓库发生了什么一目了然。2.3 为什么 pull 会在你毫无察觉时制造复杂历史这里我用一个简单的提交图来演示。假设远端origin/main是A - B - C本地main是A - D远端 origin/mainA - B - C 本地 main A - D如果你执行了一次默认的git pull等价于 fetch merge提交图会变成A - B - C \ \ D - M其中的 M 就是自动生成的合并提交。你明明只是想同步一下最新代码历史里却多了一个分支合并点。如果改用git pull --rebase提交图则是另一副模样A - B - C - DD 被“摘下来”垫到了 C 的后面生成一个新的提交 D。和原来的 D 相比D 的内容可能一模一样但它的父提交变了于是提交哈希也变了。rebase 的优势是历史保持线性读起来很舒服劣势是如果 D 已经被你 push 到公共远端再 rebase 之后又要 push远端队友会看到历史被“改写”一旦有人基于 D 做了新工作整个团队都要跟着收拾乱局。所以任何情况下对已经被推到公共远端的分支做 rebase都要格外谨慎。这个习惯不建立起来将来在多人协作里很容易出大问题。3. 我更推荐的工作流把“拉取”和“整合”拆开做3.1 先 fetch再看再决定怎么合并如果问我平时在实际项目里推荐什么我会很明确地说不要动不动就pull养成先fetch再决定下一步的习惯。这个习惯不需要安装任何额外工具也不多花多少时间只是多敲一条命令但能让你在“何时合并、如何合并”上掌握主动权。最基本的流程是git fetch origin git log --graph --oneline --boundary main origin/main git diff main origin/main第一行把远端状态同步到本地第二行用图形方式看一下本地 main 和 origin/main 的分叉情况第三行看文件层面的差异。看完之后你有至少三种选择如果远端只是多了提交你本地没有新的提交用git merge --ff-only origin/main让分支快速前进不会有额外的 merge commit。如果你本地有提交远端也有新提交希望历史干净线性用git rebase origin/main。如果你本地有提交且希望保留两条开发线合并的痕迹或者已经有人基于你的提交做了工作用git merge origin/main。这三种选择对应完全不同的仓库历史。你在pull之前有权力决定走哪条路而不是让 Git 替你默认选一种。我见过太多团队几个人不约而同地用默认pull结果同一个分支里既有 merge commit 又有 rebase 造成的重复提交互相覆盖排查问题的时候非常痛苦。3.2 用 log 和 diff 看清楚远端改了什么也许会有人问先fetch再merge和我直接pull有什么区别区别在于你有机会看一眼再动手。有些人拿到git log只会用git log --oneline下面这两个写法非常适合在fetch之后判断“远端是不是有新货”# 本地还缺哪些提交远端有、本地没有 git log --oneline HEAD..origin/main # 本地还没推到远端的提交本地有、远端没有 git log --oneline origin/main..HEAD如果HEAD..origin/main有输出说明远端有新提交如果origin/main..HEAD有输出说明你有未推送的本地提交如果两边同时有输出说明历史已经分叉接下来做 merge 或 rebase 一定会产生合并操作。想更直白地看文件差异可以执行git diff --stat HEAD origin/main一屏就能看出哪些文件有变化、变化规模有多大。这一步我一般只花十秒钟却能避免很多“我明明没改代码怎么会冲突”的疑问。越是多人协作的项目这种“看一眼再动手”的习惯越值钱。3.3 不同场景下的最佳实践我列几个实际开发中反复出现的场景给出我的处理习惯。场景一你自己在分支上开发本地有几个还没有推送的 commit这时远端被同事更新了。我的做法是git fetch之后执行git rebase origin/main或者给 Git 配置上pull.rebase true让后期pull默认采用 rebase 而不是 merge。要注意rebase 过程中如果冲突Git 会逐个提交让你解决过程比 merge 繁琐但历史确实干净。场景二在长期维护的分支上本地已经有多个提交远端也合入了别的功能的代码。此时要同步最新代码我不会直接pull而是先看一下两边的差异规模。如果差异很小考虑git merge --ff-only origin/main如果确实分叉了团队约定如果偏向线性历史就 rebase短期内能承担解决冲突的成本如果更看重稳定就 merge。没有绝对标准但一定要团队统一。场景三如果你还是希望继续用pull这个命令至少把它配置得保守一点。我个人会给pull加上--ff-only或--rebase不允许它悄悄产生 merge commit。具体做法git config pull.ff only # 或者 git config pull.rebase true前者的意思是只有当本地可以快速前进时才允许pull否则直接报错提示你手动处理后者的意思是拉取后采用 rebase 而不是 merge。具体选哪个看团队约定但肯定比默认的 merge 行为更可控也更少给你带来“历史突然多了一个合并节点”的惊吓。4. 实战中高频遇到的问题与排查经验4.1 一 pull 就冲突到底是谁的锅这是最经典的问题。原因通常是远端分支的某个提交和本地当前分支的某个提交修改的是同一个文件的同一段代码。Git 不会自作主张替你把两处改动拼在一起于是停下来等着你处理。这里的“锅”不能全甩给同事本质上是两边的修改区域出现了交集。面对冲突我的处理流程是这样的git status # 看哪些文件处于冲突状态 git diff # 查看冲突标记区域的具体内容 # 手动编辑冲突文件保留正确内容后 git add file git commit # merge 冲突解决后提交 # 如果用的是 rebase则用 git rebase --continue有几个细节想提醒一下。第一冲突标记分成三段、、分别表示当前分支内容、分隔线、对方分支内容不要只留其中一边除非你已经确认另一边确实不要了第二刚解决完冲突的时候尽量不要顺手用git commit --amend很容易把未推送的历史弄得更乱第三如果对这次合并没有信心可以在解决之前先执行git merge --abort撤销本次合并回到pull之前的状态。任何一次pull中途失败你都是有后悔机会的这正是它和fetch最大的不同意识——fetch永远不会带你走到这种状态。4.2 pull 之后多出来的 merge commit 怎么处理如果手滑执行了一次默认pull发现分支上多了一个自动生成的 merge commit而这个合并并不是你想要的可以用 reflog 找回操作前的状态git reflog # 先找到 pull 之前的那条记录比如 HEAD{2} git reset --hard HEAD{2}reflog是 Git 的后悔药机制它记录了你本地引用曾经指向过的所有提交。只要这些对象没有被 gc 清理一般都能通过 reflog 找回来。需要特别强调的是reset --hard会丢弃工作区未提交的修改执行前一定先确认工作区是干净的。注意git reset --hard会丢掉当前工作区里未提交的改动。动手之前先看一眼git status确认没有你自己不想丢的文件。另外如果这个 merge commit 已经被你 push 到公共远端了那就不要再用 reset 去改本地的引用否则会造成远端历史不一致。这种情况下更推荐用git revert做反向撤销把新的提交叠加到历史上保证团队里其他人的仓库不会崩溃。4.3 fetch 或 pull 执行很慢、卡住或没反应这种问题没有标准答案但根子上很明确fetch是要从远端服务器下载对象网络质量就是最大的变量。仓库越大、文件越多、历史越长一次fetch的耗时就越明显。尤其是 clone 了一个包含大量二进制文件的仓库之后fetch卡顿几乎是家常便饭。如果你用 IDE 的自动 fetch 经常卡先看看是不是后台还有别的任务在抢带宽再确认仓库里有没有异常大的文件。常用的排查手段有执行一次git fetch --prune origin这种写法会顺便清理已经删除的远端分支引用有时也能绕过一些旧引用带来的异常。检查自己的网络环境看看是不是有丢包、断流的情况必要时换一个更稳定的时间段再试。如果远端仓库体积实在太大可以考虑用git fetch --depth1做一次浅拉取减少传输体积但浅拉取后后续访问历史会比较受限不建议长期使用。升级 Git 到较新版本。老版本在某些协议下的性能表现差距明显升级往往是性价比最高的解法。有个容易被忽略的点如果你用 IDE 里的按钮触发 fetch它可能同时在做索引、文件监视、插件同步等额外工作慢不一定是 Git 本身的问题。这种时候打开命令行终端自己敲一遍就立刻能分清楚到底慢在哪一层。4.4 认证失败的常见报错怎么排查有时候fetch和pull不是慢而是根本没有权限。在 Windows 上用过 TortoiseGit 的人大概都见过这个报错提示no supported authentication methods available。这种报错绝大多数是 SSH 配置出了问题比如私钥没有加载、没有使用 ssh-agent、或者配置了错误的 Key 路径。按顺序做三步检查ssh -T gitgithub.com # 看能否认证成功 ssh-add -l # 查看当前私钥是否已经被 ssh-agent 加载 git remote -v # 确认当前远端地址到底走的是 SSH 还是 HTTPS如果远端地址是 HTTPS 形式则确认系统凭据管理器里有没有记住正确的账号密码如果远端地址是 SSH 形式则确认~/.ssh里的公钥私钥配对正确。还有一类不太常见但容易混淆的情况是远端仓库地址本身写错了或者对方仓库已经被删除报错日志里也会出现认证相关的字样。所以排查所有拉取类问题第一步永远是git remote -v看一眼远端地址对不对这个动作最简单也最容易救你一命。4.5 远端分支被删了本地怎么同步实际开发中很常见的情况是Web 端合并完代码后顺手把远端分支删了但你本地还一直留着origin/feature-xxx这样的引用。如果不做清理之后执行命令发现本地列表里一堆已经消失的远端分支不仅碍眼有时还会误导判断。解决办法是执行带--prune参数的 fetchgit fetch origin --prune或者使用更直白的git remote prune origin这两个操作都会把远端已经删除的引用从本地的refs/remotes/中清掉。它们不会动你本地的 feature 分支也不会动工作区里的内容所以可以放心执行。我一般习惯每次fetch都带着--prune这样本地永远看不到已经废弃的远端分支后面做分支清理的时候轻松不少。如果你平时用git branch -a看分支比较多建议把fetch --prune作为一个固定习惯。这不只是在维护命令行的整洁也是在避免你自己对着一个根本不存在的远端分支继续做基于旧引用的操作。很多人报错说“远端没有这个分支”查到最后往往是对着过期的远程跟踪分支在发呆。我个人现在几乎不会在主干分支上随手敲pull习惯是先fetch然后用git log看一眼改变再决定这一步是merge还是rebase。一开始可能觉得多了一步操作用顺手之后会发现代码历史的可控性比省那几秒钟重要得多。如果你也被“一 pull 就乱”困扰过不妨试试这套拆开做的流程。
返回列表