ARTICLE DETAIL

资讯详情

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

Git Push被拒?Merge还是Rebase?一篇讲透决策与实操

Git Push被拒?Merge还是Rebase?一篇讲透决策与实操 先聊个真实的场景。上周三下午我改完一个功能本地测试通过commit 也写得清清楚楚然后习惯性地执行git push。结果屏幕上一行红字直接让我愣住! [rejected] feature/login - feature/login (non-fast-forward) hint: Updates were rejected because the remote contains work that you do not have locally. hint: This is usually caused by another repository pushing to the same ref. hint: You may want to first integrate the remote changes (e.g., git pull) before pushing again.如果你也经常用 Git 协作这玩意儿你一定不陌生。团队里有人比你先推了代码远端分支比本地领先你的 push 被拒绝。这时候所有人都会问同一个问题我该merge还是rebase这个问题的答案没有你想的那么简单——它不只是一个命令选择背后涉及提交历史的组织方式、团队协作习惯、甚至代码 review 的流程。我这次就把整个决策过程、实操步骤、踩过的坑全部整理出来希望能帮你在下次遇到同样情况时不用再凭感觉做选择。1. Push 被拒是怎么回事从报错信息说起很多人第一次看到non-fast-forward这个关键词就懵了。其实这句报错翻译成人话就是你本地分支的历史和远端分支的历史“分叉”了Git 无法直接把你的提交“接”到远端最新提交的后面。1.1 为什么你的提交“接”不上去Git 的分支本质上是一个指向提交对象的指针push 的本质是把你本地分支上的新提交传输到远端并让远端分支指针指向这些提交。正常情况下这是一条直线我把每次 push 想象成在一条链子上挂新环本地有提交 A → B → C远端也是 A → B那么把 C 挂上去就是fast-forwardGit 很开心。但如果有同事在远端先挂了提交 D远端的链变成 A → B → D而你本地是 A → B → C这时候两条链子已经在 B 处分叉了。远端不能直接把指针从 B 移到 C因为那会丢掉 D。Git 无法确定你是想保留 D 还是丢弃 D所以它选择拒绝推送把选择权交还给你。这个设计其实是 Git 的自我保护机制。换成早期的集中式版本管理工具比如老古董 SVN服务端可能直接强制覆盖谁的代码后提交谁就赢了前一个人的工作直接消失。Git 用这种“拒绝”逼你先处理分叉虽然体验上多了一步但至少没人会莫名其妙丢代码。1.2 报错提示里的关键信息怎么读Git 的报错里其实藏着不少线索别只盯着红字发愁。我第一次遇到时也只会复制粘贴去搜后来学会了逐行看! [rejected] feature/login - feature/login (non-fast-forward) error: failed to push some refs to gitgithub.com:xxx/project.git hint: Updates were rejected because the remote contains work that you do not have locally.[rejected]后面的feature/login - feature/login表示被拒绝的是哪个本地分支推送哪个远端分支(non-fast-forward)告诉你拒绝的具体原因——历史分叉了hint:部分其实是 Git 给出的处理建议提示你“先整合远端变更再推送”。注意 hint 原文用的是git pull而不是直接告诉你该 merge 还是 rebase。原因在于git pull本身是一个组合命令默认行为等价于git fetchgit merge但你也可以让它执行git fetchgit rebase。也就是说真正的选择发生在 pull 这一步。1.3 先看一眼别急着动手我的习惯是 push 被拒之后先做一件事查看当前状态和分支分叉情况而不是凭感觉直接 pull。git status git log --oneline --graph --all -10git log --oneline --graph --all能非常直观地画出提交历史的分叉情况*代表提交点|和\代表分支走向两条线岔开的位置就是你本地和远端分叉的起点。这一步花 10 秒钟但能避免后面很多“搞不清自己 rebase 到哪了”的混乱。确认分叉情况后还会再跑一句git fetch originfetch只把远端的最新提交下载到本地存在origin/feature/login这个远程跟踪分支里不会动你的工作区更不会自动合并。这是最安全的一步相当于先“看一眼对手的牌”再决定怎么打。2. Merge 和 Rebase 的本质区别两条完全不同的“合流”思路先说结论merge 和 rebase 都能解决“远端领先、本地落后”的问题都能让你的代码与远端最新代码整合到一起但它们对提交历史的处理方式完全不同。2.1 Merge把两条路“接”成一条岔路git merge做的事情是创建一个新的“合并提交”merge commit这个提交有两个父提交一个指向你本地的分支头一个指向远端分支头。从图上看历史会形成类似这样的结构* 合并提交merge commit |\ | * 同事的提交 D * | 你的提交 C |/ * 共同的祖先提交 B * 提交 A这个结构有两个特点第一你的原始提交 C 一个都不变它们原封不动地保留在历史里第二多了一个合并提交历史会出现一个明显的“分叉再合流”的节点。merge 的优点是安全、可追溯。谁在什么时候把哪两条分支合到一起一目了然。缺点是历史会“长毛”如果团队里每个人都频繁 mergelog 图会变得极其复杂像一团没理清的毛线。很多项目后期看提交历史满屏都是Merge branch feature/xxx into dev真正的功能提交反而被淹没。2.2 Rebase把本地提交“重放”到远端最新提交后面git rebase做的事情则是把你本地分支上的提交先“摘下来”然后以远端分支的最新提交为新的起点一个一个重新应用上去。执行完后的历史是这样的* 你的提交 C新的提交哈希 * 同事的提交 D * 共同的祖先提交 B * 提交 A注意两个细节第一C 变成了 C虽然是同样的改动内容但提交哈希变了因为它的父提交从 B 变成了 D第二整个历史呈现为一条直线看起来就像你是先基于同事的提交 D再提交了你的 C。rebase 的优点是历史干净、线性review 代码时逻辑清晰git bisect二分查找出问题的提交也更好用。缺点也很明显它改写了提交历史。如果那些提交已经被推到远端、被别人拉取过rebasing 再强推会造成远端历史的“覆盖式”变化轻则让同事产生重复提交重则搞丢别人基于你旧提交所做的合并。2.3 一个生活化的类比我平时给新人讲这两个概念时喜欢用“拼积木”来类比。merge 就像你把两座积木搭好的房子并排放在一起然后用一块新的积木合并提交把两座房子连接起来。两座房子本身原封不动你一眼就能看出哪部分是自己的、哪部分是别人的。rebase 则像是把你积木上的零件全部拆下来捡起同事那座积木上最新的那一层作为新底座然后把你的零件一个一个重新拼回上去。最终看起来只有一座完整的房子但你原本那座房子的“组装痕迹”已经消失了。核心差异就一句话merge 保留“事实”rebase 整理“叙事”。如果提交历史是一本账本merge 是如实记录每笔流水rebase 则是定期把账目重新誊写一遍让账本看起来更清爽。3. 我这次到底怎么选的决策框架 完整实操光讲理论没有用我来还原我这次的实际决策过程。3.1 判断标准先看“这是谁的提交”处理被拒的 push 时第一件事不是选命令而是判断远端领先的那个提交是谁的再细分一下实际上就是三种情况。情况一远端领先的提交是同事刚推上去的功能代码。这种最普遍也是我这次遇到的。此时 merge 和 rebase 都可以用就要看团队规范和个人偏好。我们团队的规范是功能分支合入主分支用 rebase 保持线性多人协作用的共享分支强制用 merge 避免改写公共历史。这次是feature/login这种多人共同开发的功能分支所以理论上是 merge 更稳妥。情况二远端领先的提交只是别人在你 push 前几秒钟刚推上来的一点点小改动比如一行配置修复。这种场景我用 rebase 特别顺手因为改动极小冲突概率几乎为零rebase 完直接强推历史干净。情况三远端领先的提交是“不该出现”的比如同事误推了敏感信息或者推了一堆 WIP半成品提交。这种时候别急着整合先和同事沟通让他处理远端分支——该撤的撤、该修的修。你盲目 merge 或 rebase 只会把别人的问题“继承”到自己的分支里。我这次遇到的情况是第一种我们团队功能分支的历史比较乱有个人风格非常明显的“随时提交”习惯一堆fix typo、wip夹在功能提交中间。merge 的话那个含着杂七杂八提交的分叉点会一直躺在历史里rebase 的话能把我的提交干净地放到所有乱七八糟的提交之后。最终我选择了rebase 强推。理由是这是我们自己的功能分支远端没有被其他人长期基于它开发改写的风险可控而且这个分支最终要合回 dev我希望能用--no-ff合入 dev 时历史是清晰的。3.2 Rebase 方案的完整命令流程注意这里有个坑直接执行git pull默认走 merge 路线不会 rebase。想走 rebase 路线两种方式# 方式一直接带参数 pull git pull --rebase origin feature/login # 方式二先 fetch再手动 rebase我推荐这种 git fetch origin git rebase origin/feature/login我强烈推荐第二种方式原因很简单fetch和rebase分成两步每一步的执行结果都能单独验证。万一 rebase 出问题你更清楚是哪一步导致的git rebase --abort也能快速回到 rebase 开始前的状态。我这次实际执行的命令序列是# 假设当前在 feature/login 分支 git status # 确认工作区干净没有未提交的改动 git fetch origin # 拉取远端最新状态 git rebase origin/feature/login # 把本地提交重放到远端最新提交之后执行 rebase 后Git 会提示Successfully rebased and updated refs/heads/feature/login.看起来一切顺利但注意——rebase 之后你本地的提交历史已经和远端“分叉”了原来的提交 C 变成了 C哈希值变了。这时候直接git push依然会被拒绝因为远端还不知道你把历史“改写”了。你需要git push --force-with-lease origin feature/login这里又要多说一句为什么是--force-with-lease而不是--force--force是无条件强推会把远端分支直接覆盖成你本地的样子如果在你 fetch 之后又有别人往远端推了代码那个人的提交会被你冲掉。--force-with-lease则带了一个“安全校验”它会对比你本地记录的远端分支最新位置和远端实际位置只有二者一致时才允许强推如果不一致它会拒绝执行。相当于给强推加了一把锁防止误伤同事的提交。实测下来我在 rebase 后执行强推没有遇到任何问题To gitgithub.com:xxx/project.git 5a3f2b1...7c9e4d2 feature/login - feature/login (forced update)3.3 Merge 方案的完整命令流程如果你判断下来还是 merge 更稳妥或者团队规范要求用 merge命令同样简单git fetch origin git merge origin/feature/login然后 Git 会打开一个编辑器让你填写合并提交的信息默认是Merge branch origin/feature/login into feature/login这种格式一般直接保存退出就行。merge 完成后因为本地历史确实比远端领先多了一个合并提交普通git push就能通过不需要强推git push origin feature/login我之前在一个老项目里用过一年 merge 路线的协作方式最大的感受是merge 的容错率确实高。就算你在合并时手滑选错了分支、或者合并到一半不想合并了只要还没 push用git merge --abort一下就能完全回到合并前的状态。rebase 虽然也有--abort但在 rebase 中途解决冲突时心理压力会明显更大——因为你知道自己在“改写历史”一旦切错上下文容易把事情搞乱。3.4 解决冲突rebase 时的冲突处理细节rebase 最让人头大的就是中途蹦出来的冲突。这次我也没能幸免——rebase 到一半Git 报了一个冲突文件是pom.xml。rebase 和 merge 处理冲突的流程大体一致但有个关键区别要注意。merge 的冲突解决思路是“一次性处理完所有冲突然后创建一个合并提交”。rebase 则是“一个提交一个提交地重放”每重放一个提交时都可能出现冲突而且冲突要逐个解决。rebase 冲突时的命令行提示error: could not apply 3e8f12d... feat: 登录接口对接 hint: Resolve all conflicts manually, mark them as resolved with hint: git add/rm conflicted_files, then run git rebase --continue.这时候我的标准操作流程是打开冲突文件查找、、标记逐个判断该保留哪部分代码必要时打开同事的提交记录看看上下文git add标记为已解决执行git rebase --continue如果后续还有冲突重复步骤 1-4全部完成后git log --oneline检查重放后的提交列表是否符合预期。这次我的pom.xml冲突并不复杂同事加了一个新依赖我改了一个版本号两边互不干扰把两个改动都保留就行。真正麻烦的是那种“同一个文件同一段代码两边都改了逻辑”的冲突这种时候我一般会先看同事的提交说明再结合需求判断谁对谁错或者干脆约着对一下。注意rebase 过程中如果实在搞不定别硬扛。git rebase --abort可以完全退出 rebase回到 rebase 开始前的状态。记住只要还没 push你随时可以反悔。4. 实操中的常见问题与排查实录merge 和 rebase 只是 git 协作中的一小块。结合我这次被拒的经历把几个高频问题一起整理出来方便你遇到时快速定位。4.1 JSON 合并冲突为什么总在这里翻车离这次被拒不远我还在另一个项目里遇到过json merge conflict。团队多人同时改同一个接口配置 JSONrebase 时几乎必冲突。原因是 JSON 格式本身行数多、嵌套深Git 的冲突标记是按行输出的碰到一个键值对改动就可能让整个对象块标红。我的处理技巧有三条第一用 IDE 的 merge 工具而不是手改。IDEA 自带的 diff 工具会把冲突的两边上下排列配合高亮和快捷键比在终端里找快得多。第二JSON 冲突往往不是“谁对谁错”的问题而是“两边都需要保留”的问题。比如 A 加了timeout配置B 改了retryCount这时候直接把两边的内容都合并进最终文件即可。第三如果是配置文件比如package.json、lock.json我更倾向于先确认有没有人能“重新生成”这个文件。package.json 冲突时我会把两边改动都保留然后重新执行安装命令让 lock 文件自己恢复不靠手改。4.2 SSH 认证失败push 被拒的另一大类原因很多人把“push 被拒”笼统地归结为 merge/rebase 问题其实被拒还有另一大类原因SSH 认证失败。报错长这样gitgithub.com: Permission denied (publickey). fatal: Could not read from remote repository.这类报错和分支历史分叉完全没有关系你 merge 或 rebase 得再熟练也解决不了。排查顺序我建议按下面的思路来先确认远程地址是不是对的git remote -v看看是用 HTTPS 还是 SSH再用ssh -T gitgithub.com测试认证是否通过检查本地有没有生成过 SSH keyls ~/.ssh/确认 SSH key 是否已添加到 Git 平台账号中GitHub/GitLab/Gitee 的 SSH keys 配置页在新电脑或改过系统的场景下注意默认id_rsa路径和权限问题。我之前踩过一个坑换电脑后把旧的id_rsa复制过去了但权限太开放不是 600SSH 直接拒绝使用。chmod 600 ~/.ssh/id_rsa解决。这类问题表面看和 merge/rebase 无关但如果你不清楚 push 被拒的完整原因清单很容易在错误方向上浪费半小时。4.3 强推后悔了怎么撤销远端提交git push --force-with-lease之后或者更直接的git push --force之后如果发现推错了怎么办不用慌Git 有后悔药但要尽快吃。第一种情况刚 push 完发现最后一次提交有问题本地都还没继续动。可以修正本地提交后强推覆盖。比如git commit --amend -m 修正后的提交信息 git push --force-with-lease origin feature/login第二种情况已经强推了一个错误的分支状态你需要把远端恢复到之前的某个提交。前提是你本地还保留着那个提交的引用。git refloggit reflog会列出你本地所有分支指针的历史移动记录包括被 reset、rebase、强推覆盖前的旧位置。找到你想恢复的那个提交哈希然后git reset --hard 旧提交哈希 git push --force-with-lease origin feature/login我这里要特别强调reflog 只在本地有效这条命令救不了“远端已经被别人拉取并基于新历史开发”的情况。远端历史被改写后已拉取该分支的同事的本地仓库会与远端再次分叉需要他们也做一次 reset 或 rebase 才能对齐。这也是我一直强调“不要随便强推公共分支”的根本原因。4.4 IDEA 里怎么回退 merge 操作很多同事习惯用 IntelliJ IDEA 的图形界面做 git 操作。merge 之后后悔了想在 IDE 里回退有两条路。一条是“还没 commit”的路径merge 冲突解决到一半或者刚解决完还没提交合并结果此时 IDEA 的 git 面板里会有 Abort Merge 的按钮在 Git Merge 相关菜单里点击就回到 merge 前状态。另一条是“已经 commit”的路径在 IDEA 底部 Version Control 面板的 Log 标签页里找到你那个合并提交右键选择Undo Commit它会执行git reset --soft把合并提交撤销掉但保留你的代码改动在工作区。如果想连改动一起撤销用Reset Current Branch to Here...并选择Hard模式把分支指针挪到合并前的提交。IDEA 里的这些图形化操作本质上还是在调用 git 命令所以决策逻辑和命令行完全一样只是不用记命令了。我两条路都摸过比较常用的是Undo Commit因为它保留工作区改动方便重新调整后再次提交。5. 最后我在实际协作中沉淀下来的几个规范从头到尾梳理完这次 push 被拒的经历我最想说的其实是merge 和 rebase 的纠结最好的解法不是“临时决策”而是“提前立规矩”。如果团队里每个人遇到分叉都凭个人喜好选历史一定会越来越乱。我比较推荐在团队里达成下面几条约定第一功能分支feature/*内可以自由 rebase。因为功能分支通常只有你自己或少数人开发改写历史风险低。提交前整理一下提交信息合并零碎的 fix 提交让 review 的人看得舒服。第二共享分支dev、master、release/*禁止强推。一旦分支被多个人拉取它就成了“公共历史”。对公共历史做 rebase 和强推后果就是让团队其他人强制同步你的节奏这在多人协作中非常不友好。如果需要纠正公共分支上的错误优先用 revert 而不是 reset。第三pull时优先带--rebase和--autostash。我自己现在习惯在配置文件里设置git config --global pull.rebase true git config --global rebase.autoStash true这样每次git pull默认走 rebase 路线本地有未提交的改动时也不会被打断而是先自动 stash、rebase 完再自动恢复。这个习惯特别适合“本地一直有零散改动不想 commit、又需要频繁同步远端”的工作模式。命令形式就是热搜里那句git pull --rebase --autostash的长期配置版。第四push 被拒不是意外而是日常。我见过太多同事遇到non-fast-forward就慌其实在多人协作的项目里这几乎是每天都会发生的事。被拒只是 Git 在告诉你“远端有新东西了”处理它就是一次普通的同步动作不涉及任何“你的代码有问题”的判断。心态放平按流程走一遍就好。坦白说我早期也在这上面走过弯路。有一段时间我在公共分支上用过一次git push --force把同事刚推上去的提交直接冲掉了虽然没有造成严重的代码丢失但那位同事被迫花了一上午对着 reflog 找自己丢失的提交从那以后我再也没在公共分支上用过无条件的 force。这次选择 rebase 而不是 merge也是因为这些年在复盘场景中积累下来的判断——知道哪个分支可以动、哪个分支不能动比会打一百条 git 命令都重要。如果你下次再碰到 push 被拒不妨先别急着问“该 merge 还是 rebase”而是问自己一句这个分支的历史值不值得我用一次改写去换一条更直的线。
返回列表