Skip to content

fix(push): stop the pack walk at remote haves and shallow boundaries - #183

Merged
mizchi merged 1 commit into
mainfrom
claude/modest-dirac-kceqee
Sep 20, 2026
Merged

mizchi merged 1 commit into
mainfrom
claude/modest-dirac-kceqee

Conversation

@mizchi

@mizchi mizchi commented Sep 20, 2026

Copy link
Copy Markdown
Member

Fixes #182.

The bug

js_push_remote built the packfile before contacting the receiver, collecting everything reachable from HEAD. On a shallow history that walk runs off the end of what was fetched — the tip's parents are not on disk — and aborts with Missing commit object.

That is exactly the shape of a repo imported with depth: 1 (Cloudflare Artifacts): the remote is shallow, so every clone of it is shallow too, whether or not the client asked for a depth. Real git pushes to such a remote fine, because the boundary commit is already on the server and never needs to be sent.

Reproducing the issue's script against the current build, the failure is actually broader than reported — three of the four cases fail:

full remote, full clone: push ok (ac53999)
full remote, depth 1: push: Missing commit object
shallow remote, full clone: push: Missing commit object
shallow remote, depth 1: push: Missing commit object
shallow remote log: ee0b117 c3

The fix

Discover before collecting. js_push_remote now fetches the receiver's ref advertisement first and uses it as the exclusion set, which is what the native push_http path has always done. Everything reachable from an advertised ref is already on the other side, so the walk stops there — it never reaches the absent parents, and it stops re-sending history the remote already has.

Teach the collectors about shallow boundaries. collect_reachable_objects* take an optional shallow~ set; a commit in it contributes its own tree but ends the walk, the way git's grafted walk treats .git/shallow. This covers the cases the ref advertisement does not bound, such as a force-push or a branch the remote has never seen.

Record the boundary. That set only means something if it was written down, so the JS clone and fetch paths now switch to fetch_pack_with_http_result and persist the server's shallow/unshallow lines via @repo.apply_shallow_updates; fetch also reports its current boundary back to the server. The native push paths pass their boundary through too.

sync_remote_have_ids moves into bit_lib as advertised_have_ids so both push paths share one implementation.

Verification

All four cases from the issue now pass, and the shallow remote's branch really advances:

full remote, full clone: push ok (e6d695d)
full remote, depth 1: push ok (daa1a14)
shallow remote, full clone: push ok (e6d695d)
shallow remote, depth 1: push ok (daa1a14)
shallow remote log: daa1a14 from bit | e6d695d from bit | 7c8b21a c3

Tests added:

  • tools/shallow-push.test.mjs — the end-to-end guard, serving a bare repo and a --depth 1 copy of it over git http-backend and pushing to both from a bit clone. It fails on the pre-fix build with Missing commit object and passes after. Registered in Taskfile.pkl / Test.pkl; it skips itself where git http-backend is unavailable.
  • Three unit tests in pack_collect_test.mbt covering a tip whose parent object is absent, the shallow-boundary graft, and advertised_have_ids.

Suites run: moon test (1481 native + 2 wasm, all pass), moon test --target native (1483/1483), moon test --target js (1084/1086 — the two failures are the pre-existing ssh-keygen environment failures, identical on the base commit), node tools/check-layers.mjs, and the JS bundle guardrails in tools/js-build.test.mjs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VDVUJHu38YtVAr6sKZtRDB


Generated by Claude Code

`js_push_remote` packed everything reachable from HEAD before talking to
the receiver at all. On a shallow history that walk runs off the end: the
tip's parents were never fetched, so it aborted with "Missing commit
object" — the failure reported for a Cloudflare Artifacts repo imported
with `depth: 1`, where the *remote* is shallow and any clone of it is too.
Real git pushes to such a remote because the boundary commit is already on
the server and never needs to be sent.

Discover the receiver's refs first and use them as the exclusion set, as
the native `push_http` path already does. That both fixes the shallow case
and stops re-sending history the remote advertises.

Teach the collectors about shallow boundaries as well, so a walk stops at a
commit whose parents were never fetched instead of failing, the way git's
grafted walk does. The boundary is only known if it was recorded, so the JS
clone and fetch paths now write `.git/shallow` from the server's
shallow/unshallow lines, and fetch reports its current boundary back.

`sync_remote_have_ids` moves to `bit_lib` as `advertised_have_ids` so both
push paths share one implementation.

Verified with the reproduction from the issue: before, three of its four
cases failed; after, all four push and the shallow remote's branch
advances. Added as `tools/shallow-push.test.mjs`, which serves a bare repo
and a depth-1 copy of it over `git http-backend`.

Fixes #182

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDVUJHu38YtVAr6sKZtRDB
@mizchi
mizchi merged commit 92bdfe5 into main Sep 20, 2026
22 checks passed
@mizchi
mizchi deleted the claude/modest-dirac-kceqee branch September 20, 2026 10:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

push: 'Missing commit object' when the remote is shallow (Artifacts depth-1 import); git pushes fine

2 participants