- date
- entry
- 040
- topic
- tooling
- rev
- —
git push says Everything up-to-date, and the remote is a commit behind
The line after fatal: the remote end hung up unexpectedly is Everything up-to-date, and the remote is missing a commit. When it's printed, the outer git push hasn't yet folded in the failure; the earlier lines and the exit code both say it failed, and I cut them off and turned them into a 0 with | tail.
When git push prints "Everything up-to-date", it normally means none of the refs selected for this push need updating on the remote. The line is easy to read as "the push succeeded".
But on October 2, pushing a commit with 1,473 changed files in a website's repo, the last two lines of output were fatal: the remote end hung up unexpectedly and Everything up-to-date, and later records showed the remote's main still on the previous commit. On October 8 another repo printed the same two lines, and the background job reported exit code 0. This note is about how "the connection dropped" and "everything is up to date" end up in the same output. In order: the two records, a local reproduction, the reason in git's source, why I saw a 0, and how to confirm a push afterwards.
The two records
Both were HTTPS pushes to GitHub, and in both the first push used git push origin main 2>&1 | tail -2, keeping only the last two lines.
On October 2 the first push printed exactly those two lines. I then tried two common suggestions: -c http.postBuffer=524288000, and that plus -c http.version=HTTP/1.1. The retries kept the last three lines, which showed send-pack: unexpected disconnect while reading sideband packet (whether the first attempt had it too, the truncated record can't tell); the last line was still Everything up-to-date, and git rev-list --left-right --count HEAD...origin/main was 1 0 each time, meaning the local branch was one commit ahead of the remote-tracking branch. Pushing over SSH updated the remote main from the old tip to the new one, and after a git fetch the count became 0 0, which confirmed the earlier attempts hadn't landed. Packing that commit's objects afterwards with git pack-objects --revs --thin came to about 24 MB (the pack negotiated during the real push may differ).
The October 8 case was a different repo, with the push running in the background; the completion notice said exit code 0. Only reading the output showed the two lines, and git status -sb said [ahead 1]; pushing again (this time with a 110-second timeout and keeping three lines) worked. The same session also hit an ENOTFOUND reaching the API server around then, which suggests shaky connectivity at the time but doesn't establish why the GitHub push failed.
These records don't establish the root cause of either disconnect, and that isn't the point here. The point is that both times the last line said everything was fine.
Reproducing it locally: a server that hangs up part-way
I set up a local bare repo behind a Python server of about thirty lines: GET requests go to git http-backend, and a git-receive-pack POST without Content-Length (in this test, the chunked one) has up to 4,096 bytes read from the raw input before the connection is closed. The working repo has a commit with a 3 MB random file; the resulting request exceeds http.postBuffer's 1 MiB default, and -v output confirms git sends it chunked. With Apple Git 2.50.1 (the timeout used during testing is omitted below):
$ git push http main
error: RPC failed; curl 56 Recv failure: Connection reset by peer
send-pack: unexpected disconnect while reading sideband packet
fatal: the remote end hung up unexpectedly
Everything up-to-date
$ echo $?
1
The last three lines match the October 2 retries word for word (curl's error code and message were 55 or 56 across re-runs). git push -v adds POST git-receive-pack (chunked) and still ends with Everything up-to-date. --porcelain, the format meant for scripts, is blunter: its last line is Done, with exit code 1 again. The remote's main stays on the old commit.
For comparison, the local transport without HTTP: with head -c 2000 truncating the input fed to git-receive-pack, git prints ! [remote rejected] main -> main (unpacker error) and error: failed to push some refs, and no Everything up-to-date. The two setups truncate differently, so all I can say is that this HTTP reproduction goes down the helper status path described below.
The reason: the helper reports an empty result
For a smart HTTPS push like this one, git push doesn't open the connection itself; it starts the remote helper git-remote-https, which runs send-pack. Reading git v2.50.1's source, four pieces fit together like this:
rpc_service()inremote-curl.c: when the RPC fails, it reads and discards send-pack's remaining output instead of keeping it as the result, so no ref status is forwarded this time.parse_push()in the same file doesprintf("\n")after the push and then checks the result, callingexit(128)on failure. In the helper protocol that blank line means "end of ref status report".push_update_refs_status()intransport-helper.creads the helper's report line by line, stops at the blank line and returns 0. It takes the blank line as the end of the report and doesn't complain that the expected ref statuses are missing.transport_push()intransport.c:push_retis 0 andpush_had_errors()doesn't count "no status" as an error, soret = push_ret | erris 0, and its last branch,else if (!quiet && !ret && !transport_refs_pushed(remote_refs)), prints Everything up-to-date.transport_refs_pushed()treats "no status" the same as "already up to date".
The exit code arrives later. After the push, builtin/push.c calls transport_disconnect(), whose disconnect_helper() waits for the helper and collects that 128, so git push returns 1. In other words, send-pack and the helper have already printed their errors, but when Everything up-to-date is printed, the outer git push hasn't yet folded the helper's failing exit status into its result. --porcelain's Done follows the same order: it only looks at push_ret, which is still 0 at that point.
Why I saw 0: tail took the exit code
In the reproduction, git push itself returned 1 (the two original records didn't save git's own exit code). Without pipefail, the exit status of the pipeline git push … 2>&1 | tail -2 is tail's, and here tail finished normally and returned 0. The same reproduction piped through tail:
$ git push http main 2>&1 | tail -2
fatal: the remote end hung up unexpectedly
Everything up-to-date
$ echo "pipe-exit=$? pipestatus=${pipestatus[*]}" # zsh
pipe-exit=0 pipestatus=1 0
In the reproduction, tail -2 also cut the error: RPC failed; curl 56 … and send-pack lines. Of the two that remain, one says it broke and one says all is well, and the last one is easy to read as success. Going by how that pipeline was written (the record shows no sign of pipefail), the 0 the background job reported on October 8 would have been tail's.
As for http.postBuffer: git help config says raising it is only effective for disabling chunked transfer encoding, should be used only where the remote server or a proxy supports only HTTP/1.0 or doesn't comply with the HTTP standard, "is not, in general, an effective solution for most push problems", and costs memory since the whole buffer is allocated even for small pushes. On October 2 the push still dropped after raising it, which fits.
Confirming a push afterwards
Asking the remote is more reliable: git ls-remote fetches the server's current refs instead of relying on the local remote-tracking ref. In the reproduction, after the failed push the remote was at 8ba0f15 and local HEAD at d1737b7; comparing them settles it. These lines can follow a push:
set -o pipefail # works in bash and zsh; otherwise read PIPESTATUS / pipestatus
branch=$(git symbolic-ref --quiet --short HEAD) || { echo "not on a branch" >&2; exit 1; }
expected=$(git rev-parse HEAD) # record the commit being pushed, before pushing
git push origin "$branch" 2>&1 | tail -5 || { echo "push failed" >&2; exit 1; }
remote=$(git ls-remote --exit-code origin "refs/heads/$branch" | cut -f1) || { echo "remote query failed or no $branch on the remote" >&2; exit 1; }
[ "$remote" = "$expected" ] || { echo "remote is $remote, expected $expected" >&2; exit 1; }
I ran this logic in the reproduction under both bash and zsh (with the remote swapped for the fake server and for a local bare repo): when the push broke, both printed "push failed" after the tail output and exited 1; when it succeeded, the comparison passed and they exited 0. It assumes origin's fetch and push URLs point at the same repo; with a pushurl, check the end you actually push to. If the remote tip differs from what you expected, someone may also have pushed in the meantime, so take a second look.
Either check alone catches this case; I keep both. The exit code stops failures git knows about, and ls-remote stops "the remote isn't on the commit I think it is". Trimming the output is fine; just don't read only the last line.
If you remember one thing
On this failure path, "Everything up-to-date" means git received no report of a ref being updated, not that the remote has your commit. In the reproduction, the earlier lines and git's exit code both said it failed (the two original records didn't save git's own exit code); in the two cases above, | tail cut the error lines and the pipeline, without pipefail, reported only tail's 0.
Revision history
- First published