Git Push Rejected 报错全解析:从原理到团队协作实战指南
搞过一段时间 Git 的人十有八九都见过! [rejected]开头的那几行红字。尤其是赶版本、改完代码正准备提交的当口本地明明一切正常一 push 就被打回来心态瞬间就炸了。我自己刚入行那会儿遇到 Push Rejected 的第一反应是百度报错、复制粘贴、然后稀里糊涂敲一句git push --force草草了事后来在团队里把别人的提交覆盖了才意识到这不是“报错难搞”的问题而是我根本不懂 Git 在拒绝什么。这篇文章就想把 push rejected 从头到尾讲透它到底在拒绝什么、哪些场景最容易触发、每种场景对应的正确处理方式是什么以及我这些年踩坑之后沉淀下来的操作习惯和排查思路。新手看到报错不用慌按着文章里的步骤基本能一步步解掉老手也可以把它当成一份随手翻的速查手册毕竟这种报错隔三差五就会遇上一次每次重新搜太浪费时间了。1. Push Rejected 报错解析先分清它到底在拒绝什么1.1 常见报错信息速认表Git 的报错信息看着吓人其实翻来覆去就那么几类。我先把高频出现的拒绝类型列一个速查表后面再逐个展开讲原理和解决方案。你在终端里看到报错时第一件事不是到处搜而是先对照这个表确认自己碰的是哪一类。报错关键词常见触发场景处理思路fetch first/non-fast-forward远程分支上有本地没有的提交本地历史与远程历史出现分叉先同步远程内容用 merge 或 rebase 整合后再推送Permission denied/Authentication failed没有仓库写权限、SSH key 失效、HTTPS 凭据过期检查认证配置和权限设置修复后重试protected branch/push to protected推送目标分支受保护规则限制走代码评审流程或根据规则调整推送方式large files detected/exceeds file size提交了超出仓库大小限制的文件将大文件移出 Git 历史必要时配合 Git LFShook declined远程仓库 pre-receive 钩子校验未通过根据钩子提示修改提交内容或提交信息cannot lock ref/stale info远程分支引用状态过期远端已被他人强推修改过重新拉取远程引用谨慎评估是否需要强制推送这个表只能帮你快速归类真正要解决问题还得理解背后的原理。1.2 fast-forward 原理为什么 Git 要“管这么宽”大部分新手第一次遇到 push rejected配着[rejected] ... (fetch first)的提示完全不明白为什么 Git 不让推。其实核心就一个词fast-forward也就是“快进”。Git 的分支本质上是指向某个提交的指针。远程分支从 A 点走到 B 点如果你的本地提交是在远程当前 HEAD 的基础上继续向下走的那这次推送就是一条笔直的历史线远程指针能直接沿着历史往前“快进”一步。这个时候 Git 完全不会设置障碍。问题出在分叉上。假设你在本地基于 A 提交做了两个新提交 M 和 N但远程分支却已经被别人推到了 B 和 C。这时候你的本地历史是一条线远程历史是另一条线两条线在 A 处分叉了。如果你强行把本地推上去Git 需要做一个非常危险的动作把远程的 B、C 提交全部丢掉然后强制把分支指针移到你的 N 上。这不叫推送叫覆盖。为了保证仓库历史不被人为抹掉Git 默认拒绝这种操作并提示你“远程有你本地没有的提交”。所以push rejected本质上是 Git 在保护远端数据不是故意给你添堵。理解了这一层后面所有的解决方案都围绕着同一个目标让远程的历史成为本地历史的一部分重新让推送变成一次干净的快进。2. 非快进推送Non-Fast-Forward的完整处理流程2.1 三步定位先看清本地和远程的分叉情况遇到推送被拒第一步永远不是闷头敲命令而是把状态摸清楚。我一般按三个动作来定位问题。先git fetch origin只拉取远程状态不合并任何内容。这一步非常安全不会改动你的工作区只是让本地知道远程现在长什么样。接着用git status看本地与远程的关系Git 会明确告诉你“您的分支和 origin/main 分叉了分别有 2 个和 3 个不同的提交”。最后用git log --oneline --graph --all -10看一下提交历史的分叉和走向确认分叉点在哪里。这三步做完你的问题基本就清晰了落后远程几个提交本地有几个独有提交分叉点在哪个位置。清楚了再决定下一步千万别跳过 fetch 直接 pull至少在团队协作环境里直接 pull 可能会产生一个你想不到的合并提交后面我会详细说。2.2 方案选择merge、rebase、强推到底怎么权衡分叉确定之后主流方案就三个git pull默认 merge、git pull --rebase变基、git push --force强推。我见过很多人在这一步凭感觉瞎选结果越搞越乱所以这里一次性把三种方式的适用场景讲清楚。先看 merge。执行git pull拉取远程并自动创建一个 merge commit把两条分叉历史重新缝合起来。它的好处是完整保留了时间线适合那种“我需要明确保留分支合并痕迹”的场景比如公共分支的合入记录。坏处是历史会多出不少“Merge branch”之类的提交时间一长git log --graph就像一团毛线。个人开发或者功能分支上我不太推荐这种默认方式。再看 rebase。执行git pull --rebase会把本地独有的提交先“摘下来”放到远程最新提交后面重新应用一遍让历史变成一条笔直的线。这样最终推送时就是一次干净的 fast-forward历史可读性好也最容易排查问题。代价是你本地的提交相当于被重建了一次如果这些提交同时存在于别的地方可能会引起一些混乱。但大多数团队场景下rebase 是更理想的选择。最后是git push --force。这是强推等于直接告诉 Git“别管远程的回溯保护把分支指针移到我的位置上”。它只适合非常有限的场景确认远程分支上的提交已经完全没用了且没有任何人依赖它。比如你自己开的一次性 feature 分支或者刚推上去就发现方向错了、想重写历史。公共分支上一律禁止强推这是我非常强调的一条底线下面会专门展开讲。2.3 一次完整的 rebase 冲突解决实操方案确定之后真正执行时最怕什么最怕 rebase 过程中弹出一堆冲突。我第一次做 rebase 看到CONFLICT就慌了以为把仓库搞坏了其实这完全是正常的整合动作。下面给一个可以直接照着敲的标准流程。git fetch origin git rebase origin/main # 如果出现冲突Git 会列出冲突文件 # 编辑冲突文件保留需要的内容删除冲突标记 git add 冲突文件 git rebase --continue # 如果有多个提交Git 会依次重放可能多次需要处理冲突 git push origin main这里有几个细节容易被忽略。第一冲突解决后千万不要自己手动去git commit而是用git addgit rebase --continue让 rebase 流程自己生成提交。第二如果 rebase 中途你觉得思路乱了随时可以git rebase --abort回到 rebase 之前的状态从头再来也不丢数据。第三如果同一个冲突文件在多个提交里反复出现可以在动手之前先开启 rereregit config --global rerere.enabled true。这个功能会自动记录你解决冲突的方式并在后续冲突中尝试复用我实测下来能显著减少重复劳动。还有一个很实用的建议rebase 之前先看一眼本地独有的提交数量。如果只有一两个提交冲突处理通常很快如果有几十个提交且历史跨度很大建议换个思路比如在远程最新基础上重新落代码或者拆分成小而稳的提交再逐个 rebase。3. 权限不足、分支保护与其他隐蔽拒绝原因3.1 认证与权限问题排查非快进推送是最常见的拒绝原因但远不是唯一原因。权限和认证问题在多人协作仓库里也经常出现而且这类报错很容易被误判为网络问题或配置问题。HTTPS 方式时最常见的报错是remote: HTTP Basic: Access denied或者Authentication failed。如果你确定用户名密码没问题那大概率是密码更新了但本地的凭据还存着旧值。Windows 上要去控制面板的凭据管理器里删掉旧的 Git 凭据macOS 上使用钥匙串管理Linux 上要清理~/.git-credentials。删完之后下一次操作会触发重新输入用户名密码的提示输入新的凭证就能解决。SSH 方式时常见的报错是Permission denied (publickey)。先确认本机有没有生成过 SSH keyls -l ~/.ssh看到一个.pub结尾的文件就是公钥。然后把公钥内容复制到 Git 托管平台的 SSH Keys 设置里。如果 key 本身存在但仍报错用ssh -T gitgithub.com或者对应平台的类似命令测试一下连接这个命令会直接反馈 key 是否被服务端认可。还有一种容易被忽略的情况你本地对某个仓库有推送权限但今天 clone 的是别人的 fork 仓库。Git 只会在你 push 的那一刹那告诉你“没有写权限”日常开发根本感觉不到。所以看到权限类报错时第一反应应该是确认当前 remote 地址是不是你真正要推的那个仓库git remote -v一条命令就能看清来源。3.2 分支保护规则Protected Branch的正确玩法现在稍微正规一点的团队都会在主分支上开启分支保护。GitHub、GitLab 和 Gitea 这些平台都有类似功能常见规则包括不允许直接推送主分支、必须通过 Pull Request / Merge Request 合入、要求代码评审通过、要求 CI 检查通过、不允许强制推送等。如果你 push 时看到报错里带protected branch别想着用--force绕过这属于规则设计好的防线绕过它既违背团队规范也容易出了事故找不到责任边界。正确做法是走流程新建一个功能分支把代码推上去然后发起合并请求。推功能分支通常不受保护规则影响这也是为什么我强烈建议所有改动都优先发到 feature 分支别直接在 main 上做开发。还有一种情况是分支保护规则的细节设置问题。比如规则要求“必须至少一位 Reviewer 通过”但你在一个小团队里一时找不到人评审这时候可以和仓库管理员沟通临时调整规则或直接获得授权而不是去找各种办法绕过保护机制。3.3 大文件与钩子检查被 CI 拦下来的情况另一类很隐蔽的拒绝原因来自仓库层面的检查。最典型的是大文件检测。GitHub 和 GitLab 都对单文件大小有限制一般在 50MB 到 100MB 之间。你本地提交了一个大视频或大模型文件push 时服务器直接给你打回报错关键词通常是large files detected。遇到这种情况如果你还有后续提交单纯git rm掉当前文件是不够的因为大文件已经写进提交历史了历史里依然留着这个“雷”。需要把历史里所有包含大文件的提交重写掉通常借助git filter-repo或者 BFG Repo-Cleaner 这类工具。重写历史等于制造新的提交哈希后续需要协调团队统一重新拉取不是一个人偷偷能搞定的操作。如果你确实需要管理二进制大文件更合理的方案是引入 Git LFS把大文件存储机制从 Git 对象库里剥离出来仓库本身就不会超限。另一类是 pre-receive 钩子检测。服务端钩子会在接收提交前做校验比如检查提交信息是否符合规范、检查分支命名、甚至执行一些自定义的脚本检查。这类报错通常会给出明确的提示文字比如remote: *** Error: commit message is not in the correct format。处理方法很直接按照提示修改提交信息。注意要修改的是提交信息不只是重新提交一次需要用到git commit --amend或者 rebase 后重新设置 message然后再推送。3.4 其他容易忽略的拒绝场景除了上面几种还有几个略微冷门但真实存在的情况值得提一嘴。stale info是一个特别容易卡住老手的报错。它出现的前提是远程分支的引用在你本地已经过期而且远程分支被其他方式重置过常见于有人之前强推覆盖了该分支。报错会提示cannot lock ref意思是远程 ref 的状态和你预期的不一致。解决办法是先git fetch origin拉取最新引用再重新评估自己需要怎么推。如果确实需要覆盖也要先用--force-with-lease而不是裸--force。还有一种是浅克隆shallow clone导致的推送问题。如果你用git clone --depth 1拉下来的仓库本地缺少完整的祖先历史在某些情况下推送会被拒绝。解决思路是先把浅克隆补全用git fetch --unshallow拉取全部历史然后再执行推送。最后提一下 submodule 场景。如果仓库里有 submodule而 submodule 的远端更新了主仓库推送时不带 submodule 的更新可能不会出问题但反过来 submodule 本身推送失败主仓库的 push 也会连带失败。这种情况需要先进入 submodule 目录按照上述流程解决它自己的拒绝问题再回到主仓库重新 push。4. 从流程上避免 Push Rejected团队协作与个人习惯4.1 推送前自检清单Push Rejected 最磨人的地方不是解决起来多难而是它总会出现在你毫无防备的时候。想从根上减少这类问题我建议把“推送”这件事当成一个有前置条件的动作养成固定的自检习惯。下面这份清单是我个人每天都在用的抄作业即可。第一提交前先看一眼所在分支git status确认自己不在受保护的主分支上直接开发。第二推送前先同步远程状态git fetch origin如果本地落后立刻做git rebase origin/当前分支名再继续。第三养成小步提交的习惯每次提交只做一件事提交信息写清楚为什么这样 rebase 和排查都会容易得多。第四推送后看一眼结果确认fast-forward而不是[rejected]。第五如果要合入主分支优先功能分支 MR/PR让远程保护规则替你兜底。这套习惯看起来平淡无奇但能覆盖掉绝大多数因历史分叉导致的 push rejected。实测下来我个人的推送被拒频率比刚入行那会儿下降了大概九成。4.2 低冲突的日常协作流理想的团队协作流应该是“分布式开发、低耦合合并”。我最推荐的模式是主分支长期稳定所有人都在功能分支上开发功能完成后再通过合并请求回归主分支。但很多团队在实际操作中仍然很多人直接在主分支上改于是主分支成了冲突高发区push rejected 自然频繁。一个健康且低冲突的流程大概长这样每天上班第一件事git fetch origin然后基于最新的主分支创建或更新自己的功能分支。开发过程中每个功能点完成一个提交就立即 push 到远程的功能分支上这样即使中间出了岔子提交也都在远端有备份。功能临结束时再次git rebase origin/main把你的功能分支变基到最新主分支上逼着你提前把冲突解决在自己分支里而不是等别人合并时才发现。最后发 MR/PR让代码评审把关质量。这套流程下push rejected 出现的概率会很低因为每次 push 前你都主动同步过远程状态。即便是 rebase 时出现冲突处理的也是你自己的功能分支影响面可控不会干扰别人。4.3 强推是双刃剑安全强推与止血救援虽然我一直强调强推要谨慎但不得不承认某些场景免不了要重写历史所以必须学会安全地强推而不只是知道--force这么简单。Git 提供了一个保护性更强的参数git push --force-with-lease。这个参数会在推送前先检查远程分支的引用是否还是你上次 fetch 时的状态如果别人在你之后有新推送它不会无声无息地覆盖掉对方提交而是拒绝推送并提示你重新同步。它比裸--force强在所有碰撞都会被提前拦截。所以如果必须强推永远优先--force-with-lease。再讲一下强推事故后的止血方案。假设你或你的同事真的强推过公共分支把别人的提交覆盖掉了千万别慌也别重启电脑。Git 的提交不会立刻消失只要对应提交还存在于某个本地仓库的 reflog 里就能找回来。先git reflog找到被覆盖前的提交哈希然后用git branch 恢复分支名称 那个哈希把被删的提交重新拉回一个分支最后再做一次安全强推把分支恢复到正确位置。操作本身不难难点在于别在事故发生后的混乱中连续做更多错误操作。我在实际工作中见过很多次“救命式强推”本来只要强推一次结果强推失败后慌乱中不用--force-with-lease又硬推了好几次把现场越搅越浑。正确的止血姿势是先停下来把所有人都拉到同一信息基线确认谁有正确的提交、正确的哈希是什么然后再选择一个精确的时刻恢复。5. 排查速查表与实战心得5.1 Push Rejected 常见问题速查表前面内容比较长最后我把所有场景压缩成一张速查表方便你下次遇到问题直接对照着处理。报错特征判断依据首选方案本地和远程有分叉提示 fetch firstgit fetch后git status显示分叉git rebase origin/分支名后重新推送远程有强制推送到本地没有的提交报错包含non-fast-forward同步远程后整合不建议直接覆盖权限不足报错包含Permission denied检查 SSH key 或 HTTPS 凭据、仓库权限分支被保护报错包含protected branch改用功能分支走 MR/PR 流程提交中有大文件报错包含large files detected用 filter-repo 清理历史或改用 Git LFS提交信息不符合规范报错包含hook declined修改提交信息后重新推送引用锁定失败报错包含cannot lock ref重新git fetch谨慎评估后操作浅克隆推送受限本地仓库是浅克隆git fetch --unshallow补全历史这张表不能覆盖所有千奇百怪的情况但解决了 95% 的日常问题剩下的漏网之鱼多数也能站在“Git 在保护远端数据”的视角想明白原因。5.2 三个真实场景复盘场景一一个人开发一个老项目。本地两星期没动过远程多了几个别人的提交于是一 push 就被打回。很多人这时候会直接git pull结果被拖进一个莫名其妙的合并提交里历史乱成一团。正确处理是git fetch git rebase让自己的提交干净地坐在远程历史上面历史平直没有多余的 merge 节点。场景二团队合作中功能分支推送到远程没问题但在发起 MR 后发现目标分支已经有新提交显示冲突。这个我前面说过本质上还是历史分叉只是冲突暴露在了合并阶段。务实做法就是把目标分支拉回来在你的分支上git rebase 目标分支把冲突消化在自己的分支里再重新 push 功能分支MR 里的冲突提示就会消失。场景三同事强推把公共分支回滚导致别人的提交丢失。恢复时就是用git reflog找回来再把分支重新指回去。这个场景最重要的经验就是别慌先检查 reflog 里有没有正确的提交再考虑要不要通知仓库管理员介入。很多版本回滚事故没能恢复不是因为提交真的消失了而是因为有人在恢复过程中不断提交覆盖把 reflog 的路径打断了。5.3 我的操作习惯与最后想说的话从最开始看到 push rejected 就头皮发麻到后来基本能一眼定位原因我的体会是这类报错最好把它当成远程仓库给你的一次健康检查。每次被拒都在提示一个信息——你的同步节奏落后了、你的历史存在分叉、你的提交触发了某条规则。与其和它硬刚不如停下来想想它想告诉你什么。如果只让我留一条建议我会反复强调这句话先同步、再推送、别硬覆盖。把git fetch当成日常动作把--force-with-lease焊死在肌肉记忆里push rejected 就会从一个让人焦头烂额的问题变成一条几分钟就能走完的常规流程。这也是我写这篇文章最想传达的东西。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →