两个人改同一个文件,一个人 push 成功、另一个人被拦,通常不是“谁改得对”的问题,而是两件独立的事叠在一起:本地历史与远端分叉(冲突类),以及分支保护规则不允许直接推送(权限类)。判断顺序建议先读 push 报错文本,再决定是回本地 fetch 后合并或变基,还是去仓库设置页核对规则。不要一上来就 git push -f,强制推送既绕不过权限拦截,也可能覆盖别人的提交。
先看 push 报错里的关键词:出现 non-fast-forward、behind、fetch first,属于历史分叉,在本地 fetch 后 merge 或 rebase 再推;出现 protected branch、not allowed to push、pre-receive hook declined、remote rejected,属于规则拦截,改代码没有用,要去分支保护或规则设置页确认谁有直推权限。两类也可能同时出现,先解决分叉,仍被拒再查规则。判断依据是报错文本、git log 对比结果和设置页配置,不靠猜测。
先读被拒时的报错文本属于哪一类
动手改代码之前,先把终端里那段报错原样读完。两类拒绝在文本上有明显区别,先看有没有 [rejected] 和 [remote rejected] 这个前置标记。
冲突类(非快进)通常长这样,对象已经被本地客户端判断拒绝:
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to 'git@example.com:team/repo.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
权限类通常来自服务端钩子,前缀是 remote: 或 ! [remote rejected]:
remote: error: Protected branch update failed for refs/heads/main.
remote: error: Changes must be made through a pull request.
! [remote rejected] main -> main (protected branch hook declined)
remote: GitLab: You are not allowed to push code to protected branches on this project.
判断规则:出现 non-fast-forward、behind、fetch first,下一处该看的是远端分支的最新提交,直接跳到第二节;出现 protected branch、not allowed to push、pre-receive hook declined、Changes must be made through a pull request,下一处该看的是仓库的分支保护或规则设置页,跳到第四节。两段文本同时出现时,先按分叉处理,处理完再推一次,看是否还剩下权限报错。
在本地复现历史分叉的场景
目标是确认本地分支和远端分支是不是真的从同一个提交点分开了。先抓取远端最新状态,注意 fetch 只更新远端跟踪分支,不动你的工作区:
git fetch origin
git branch `--show-current`
git log `--oneline` `--graph` `--left-right` origin/main...main
回显判读:行首是 < 的提交只在远端有,是别人刚推上去的;行首是 > 的提交只在本地有,是你自己做的;两边都出现,说明历史已经分叉。如果只有 < 没有 >,那就是单纯的落后,直接合并远端即可,不需要变基。
想再确认一次共同祖先,可以用:
git merge-base origin/main main
git rev-parse origin/main main
如果 git merge-base 的结果等于 origin/main 的 SHA,说明本地是在远端之上加提交,属于可快进;如果等于 main 的 SHA,说明本地落后;两者都不等于其中任何一端的 SHA,才是真正的分叉点。把这两条命令的输出贴到排查记录里,比口头描述“好像冲突了”更省事。
解决冲突并重新推送
确认分叉后,把两条历史收敛回一条。合并路径保留双方提交,适合共享分支:
git fetch origin
git switch main # 旧版本 Git 用 git checkout main
git merge origin/main
# 冲突时用下面这条列出待处理文件
git diff `--name-only` `--diff-filter`=U
# 逐个编辑冲突文件,去掉 <<<<<<< ======= >>>>>>> 标记
git add 冲突文件
git commit
git push origin main
变基路径把本地提交挪到远端之后,历史更直,但会重写本地提交的 SHA:
git fetch origin
git rebase origin/main
# 冲突处理完
git add 冲突文件
git rebase `--continue`
# 中途想放弃:git rebase `--abort`
git push origin main
几个容易踩的点:冲突处理完必须 git add,否则 rebase `--continue` 会重复报同一个冲突;git status 里出现 Unmerged paths 说明还没处理干净;变基后推送被拒且确认只有自己在用这条分支,可以先用 git push `--force-with-lease`,它会在远端被别人更新过时失败,比 -f 保守,但共享分支上仍然建议先和对方打招呼。推送成功后,让对方 git fetch 再拉一次,避免他继续基于旧提交改。
确认对方为什么被放行
同一个文件、同一次改动范围,一个人能直推、一个人被拦,差异在规则不在代码。常见三类规则分布在设置页的不同位置,逐项对照比争论有效:
- 分支保护:GitHub 在仓库 Settings → Branches 或 Rulesets,GitLab 在 Settings → Repository → Protected branches,Gitea/Gogs 在仓库设置 → 分支保护,Bitbucket 在 Repository settings → Branch permissions。看保护的是具体分支名还是
release/*这类通配模式。 - 直接推送权限:GitLab 的保护分支页面有 Allowed to merge 和 Allowed to push 两列,GitHub 分支规则里有 Restrict who can push 以及是否允许管理员绕过。确认谁在白名单里、是用户还是角色。
- 评审要求:是否需要先开 PR/MR、最少评审人数、是否要求状态检查通过。这一项通常写在与上两项同一个页面或同一个规则详情里。
逐项对照的方法:先在本地确认自己推的是哪个远端和哪个分支(git remote -v、git branch `--show-current`),再到对应仓库设置页找到同名或匹配的分支规则,一条条核对“你能直推这条分支吗、需要评审吗、会绕过吗”。如果设置页看不到,通常是权限不够,找仓库管理员确认,而不是反复重试 push。
把分支保护规则当作前提写进协作约定
这类问题反复出现,多数是因为规则只存在于设置页,没写进仓库说明。建议在 README 或 CONTRIBUTING 里用可执行的条目写清楚,例如:
## 分支与推送约定
- main:禁止直接推送,一律通过 PR 合并,至少 1 名评审通过。
- release/*:仅发布负责人可推送,其他人走 PR。
- feat/*、fix/*:本人可自由推送,禁止对他人分支 force push。
- 合并方式:默认 squash merge,提交信息带 issue 编号。
- 推送被拒时:先 git fetch,再 rebase origin/<branch>;
若报错含 protected branch / not allowed to push,
贴完整报错到协作群,由管理员确认权限,不要使用 -f。
写的时候注意两点:条目要能对应到设置页里的实际开关,别写成和配置不符的口号;涉及发布分支的规则,注明负责人变更是谁维护。这样新人遇到被拒,先看仓库说明里的这段,再决定是回本地解决分叉还是找人确认权限,能少绕一圈。