git push 到底向服务端发了什么?--force 和 --force-with-lease 有什么区别、后者有什么盲区?服务端怎么拒绝你?这篇用本机的裸仓库和两个客户端 A、B 把这些场景逐个重现。
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 没被别人改过。
$ 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 之后才会清理。
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 和工作区与推送的内容不一致;所以服务器上的仓库应该是裸仓库。
$ 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 要求全部成功,否则全部不更新。