bigywater.tech
Git · 66

push 的 receive-pack、非快进拒绝与 --force-with-lease

git push 到底向服务端发了什么?--force 和 --force-with-lease 有什么区别、后者有什么盲区?服务端怎么拒绝你?这篇用本机的裸仓库和两个客户端 A、B 把这些场景逐个重现。

旧值 新值 ref
push 发给 receive-pack 的命令格式,服务端用旧值检查 ref 没被别人改过
stale info / 放行
--force-with-lease 在没 fetch 时拒绝,fetch 过但没合并时放行并覆盖 B1
remote rejected
服务端拒绝的标记,区别于客户端自己判断的 rejected

协议

实测 · 本机执行输出
  packet: receive-pack> <sha> refs/heads/main\0report-status report-status-v2 delete-refs side-band-64k quiet atomic ofs-delta object-format=sha1 agent=git/2.54.0
  packet: push< <sha> refs/heads/main\0report-status report-status-v2 delete-refs side-band-64k quiet atomic ofs-delta object-format=sha1 agent=git/2.54.0-Darwin
  packet: push> <sha> <sha> refs/heads/main\0 report-status-v2 side-band-64k quiet object-format=sha1 agent=git/2.54.0-Darwin
  packet: receive-pack< <sha> <sha> refs/heads/main\0 report-status-v2 side-band-64k quiet object-format=sha1 agent=git/2.54.0-Darwin
  packet: receive-pack> unpack ok
  packet: receive-pack> ok refs/heads/main
  packet: push< unpack ok
  packet: push< ok refs/heads/main

push 时服务端 receive-pack 先广告它的 ref;客户端发命令 旧值 新值 ref名,再发 pack;服务端解包后回 unpack ok 和每个 ref 的 ok/ng。旧值保证服务端的 ref 没被别人改过。

非快进与 --force

实测 · 本机执行输出
  $ git push origin main 2>&1 | grep -E "rejected|error|hint: Updates" | sed -E "s#/work/[a-zA-Z0-9._]*#<路径>#g"
   ! [rejected]        main -> main (fetch first)
  error: failed to push some refs to '/work/l62b.git'
  hint: Updates were rejected because the remote contains work that you do not
  强制推送 --force:成功,远程的 main 被改成 A1,B1 不再被 main 引用
  $ git push --force origin main 2>&1 | grep -E "\+|forced" | sed -E "s/[0-9a-f]{7}/<sha>/g; s#/work/[a-zA-Z0-9._]*#<路径>#g"
   + <sha>...<sha> main -> main (forced update)
  远程 main 上的提交: A1 → M1
  B1 的提交还在远程的对象库里: True;远程 main 包含 B1 吗: False

客户端根据广告的 ref 值判断不是快进,就在发送前报 fetch first 拒绝。--force 无条件覆盖,被覆盖的提交对象仍在远程对象库里,只是没有分支引用,gc 之后才会清理。

--force-with-lease

实测 · 本机执行输出
  A 没有 fetch(远程跟踪分支还是旧的),--force-with-lease 被拒绝:
  $ git push --force-with-lease origin main 2>&1 | grep -E "rejected|stale|error" | sed -E "s#/work/[a-zA-Z0-9._]*#<路径>#g"
   ! [rejected]        main -> main (stale info)
  error: failed to push some refs to '/work/l62c.git'
  A 先 git fetch 看到 B1,但没有合并;此时 --force-with-lease 认为“我已经见过远程的值”,放行并覆盖 B1(它的弱点):
  $ git push --force-with-lease origin main 2>&1 | grep -E "\+|forced" | sed -E "s/[0-9a-f]{7}/<sha>/g; s#/work/[a-zA-Z0-9._]*#<路径>#g"
   + <sha>...<sha> main -> main (forced update)
  更安全的写法:显式写出期望的旧值 --force-with-lease=main:<sha>(sha 来自你实际检查过的那次 fetch),或者用 --force-if-includes
  $ git push --force-with-lease --force-if-includes origin main 2>&1 | grep -E "rejected|stale|error|forced" | sed -E "s/[0-9a-f]{7}/<sha>/g; s#/work/[a-zA-Z0-9._]*#<路径>#g"
   ! [rejected]        main -> main (remote ref updated since checkout)
  error: failed to push some refs to '/work/l62d.git'
  hint: Updates were rejected because the tip of the remote-tracking branch has

只有远程 ref 等于你最后一次 fetch 的值才覆盖,没 fetch 就报 stale info。弱点:如果已经 fetch 过(看到了别人的提交)但没合并,它仍会放行并覆盖。--force-if-includes 额外要求本地历史包含远程跟踪分支的新值,才能拒绝这种情形。

服务端拒绝非快进

实测 · 本机执行输出
  $ git push --force origin main 2>&1 | grep -E "rejected|remote:|error" | sed -E "s/[0-9a-f]{7}/<sha>/g; s#/work/[a-zA-Z0-9._]*#<路径>#g"
  remote: error: denying non-fast-forward refs/heads/main (you should pull first)
   ! [remote rejected] main -> main (non-fast-forward)
  error: failed to push some refs to '/work/l62e.git'

receive.denyNonFastForwards 让服务端拒绝所有非快进,--force 也无效,显示 remote rejected(区别于客户端自己判断的 rejected)。

推到当前检出的分支

实测 · 本机执行输出
  $ git push origin main 2>&1 | grep -E "remote:|rejected|error" | cut -c1-110 | sed -E "s#/work/[a-zA-Z0-9._]*#<路径>#g"
  remote: error: refusing to update checked out branch: refs/heads/main
  remote: error: By default, updating the current branch in a non-bare repository
  remote: is denied, because it will make the index and work tree inconsistent
  remote: with what you pushed, and will require 'git reset --hard' to match
  remote: the work tree to HEAD.
  remote:
  remote: You can set the 'receive.denyCurrentBranch' configuration variable
  remote: to 'ignore' or 'warn' in the remote repository to allow pushing into
  remote: its current branch; however, this is not recommended unless you
  remote: arranged to update its work tree to match what you pushed in some
  remote: other way.
  remote:
  remote: To squelch this message and still keep the default behaviour, set
  remote: 'receive.denyCurrentBranch' configuration variable to 'refuse'.
   ! [remote rejected] main -> main (branch is currently checked out)
  error: failed to push some refs to '/work/l62nb'

非裸仓库当前检出的分支默认不允许被推送(receive.denyCurrentBranch),因为会让 index 和工作区与推送的内容不一致;所以服务器上的仓库应该是裸仓库。

--atomic

实测 · 本机执行输出
  $ git push origin main other 2>&1 | grep -E "^ [!*+ ]|rejected|\[new|->" | sed -E "s/[0-9a-f]{7}/<sha>/g; s#/work/[a-zA-Z0-9._]*#<路径>#g"; git ls-remote origin other | wc -l | sed "s/^ */远程 other 分支存在: /"
   * [new branch]      other -> other
   ! [rejected]        main -> main (fetch first)
  hint: Updates were rejected because the remote contains work that you do not
  远程 other 分支存在: 1
  $ git push --atomic origin main other 2>&1 | grep -E "rejected|atomic|\[new|error" | sed -E "s/[0-9a-f]{7}/<sha>/g; s#/work/[a-zA-Z0-9._]*#<路径>#g"; git ls-remote origin other | wc -l | sed "s/^ */远程 other 分支存在: /"
  error: atomic push failed for ref refs/heads/main. status: 5
   ! [rejected]        main -> main (fetch first)
   ! [rejected]        other -> other (atomic push failed)
  error: failed to push some refs to '/work/l62f.git'
  hint: Updates were rejected because the remote contains work that you do not
  远程 other 分支存在: 0

一次推送多个 ref 时,默认各自成功或失败;--atomic 要求全部成功,否则全部不更新。

分步演示

push 与 receive-pack
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。服务端是本机的裸仓库(文件系统路径,没有走网络),A、B 是两个克隆;协议跟踪用 GIT_TRACE_PACKET;输出里的路径与 hash 做了替换
    • 没有验证:push 的服务端 hook(pre-receive、update、post-receive,见 hooks 一篇);通过 ssh/http 的认证流程;--force-with-lease=:<期望值> 的显式写法;push.default 与 push -u 的行为;receive.denyDeletes;shallow 仓库 push 的限制
    京ICP备2026067502号-1京公网安备11010502063487号