fix: UploadPackV2 emits `ready` (or omits acks) when client sends `done` #18

merged colechristensen cole.christensen@gmail.com wants to merge fix/upload-pack-v2-done-ready into main
No CI

Closes #27.

Summary

git fetch against a repo served by UploadPackV2 was failing with

fatal: expected no other sections to be sent after no 'ready'

whenever the client’s local history was unrelated to what’s on the server (reproduced against fangorn/hephaestus on 2026-04-18).

The server was emitting an acknowledgments section ending in NAK, followed by a packfile section. That’s a protocol-v2 violation: the spec says the acks section must end with ready if a packfile follows — otherwise the section must be omitted entirely.

Fix

handle_fetch/2 now parses done from the fetch args and build_acknowledgments/3 branches on it:

client state response
done + no matching haves acks section omitted, packfile follows
done + some ACKs acknowledgments / ACK … / ready / delim + packfile
no done, no ACKs yet acknowledgments / NAK / flush (multi-round continues)
no done, some ACKs acknowledgments / ACK … / flush (multi-round continues)

Tests

Two layers, both new or updated in this PR:

  1. State machinetest/ex_git_objectstore/protocol/upload_pack_v2_test.exs

    • with \done` and no matching haves: acks+packfile response must include `ready`` — the exact protocol invariant from the spec
    • with \done` and no matching haves: packfile is sent (client wants unconditional send)` — locks in that a fix can’t just drop the packfile
    • no common ancestors + done: omits acks section and sends packfile — updated to reflect correct behaviour
  2. Real git client interop — new file test/ex_git_objectstore/integration/upload_pack_v2_git_client_test.exs

    • Spins up an in-process git:// TCP daemon over UploadPackV2 and runs the real git binary against it
    • clone of a single-commit repo succeeds
    • clone of a multi-commit chain succeeds
    • fetch works when client has unrelated history (hephaestus regression)
    • fetch after clone is a no-op (matching haves)

Before the fix, the hephaestus-regression test reproduces the exact client error. After the fix, all four interop tests pass.

Test plan

  • mix test --include integration — 713 / 713 pass locally
  • mix format --check-formatted
  • mix credo --strict on touched files — no new issues
  • Downstream: anvil still compiles with the local path dep
  • CI passes on this branch
Created Apr 18, 2026 at 22:21 UTC | Merged Apr 18, 2026 at 22:31 UTC by colechristensen cole.christensen@gmail.com