feat(lfs): git-lfs-authenticate SSH handoff + fix locking over SSH remotes #42

merged colechristensen cole.christensen@gmail.com wants to merge fix/lfs-ssh-authenticate into main

Closes #65 Closes #66

Problem

Reported: over an SSH remote, git lfs prints “Remote does not support the Git LFS locking API” and git push hangs.

Root cause

Git LFS does not transfer over SSH. For an SSH remote the client runs git-lfs-authenticate <path> <upload|download> over the SSH connection and uses the returned JSON to discover the HTTP LFS endpoint, then drives Batch / transfer / locks over HTTP.

The library shipped no handler for this command (rg git-lfs-authenticate returned nothing). So an SSH client could not discover the endpoint — it fell back to a guessed HTTPS URL, POST /locks/verify failed (→ the “not supported” message), and the object transfer hung against the unreachable guess. This was invisible because every existing LFS interop test sets an explicit lfs.url and pushes over HTTP (issue #58 scoped LFS to HTTP + PAT auth only; SSH was never considered).

Reproduced directly with the real git lfs 3.7 binary before fixing — a missing/guessed authenticate href produces exactly the reported symptom (connection refused on batch + “does not support locking API”).

Changes

  • #65ExGitObjectstore.Lfs.Authenticate.handle/3: a pure module (same style as Batch/Transfer/Locks) that builds the spec JSON (href, header, expires_in/expires_at) a server writes to stdout for the SSH command. The library does not invent an auth scheme: the consumer supplies :href (it alone knows its public HTTP base) and any :header; the module validates operation (:bad_operation), href (:missing_href) and lfs-configured (:lfs_not_configured). Telemetry at [:ex_git_objectstore, :lfs, :authenticate].
  • #66InteropSshTest: drives real git lfs over an ssh:// remote with no explicit lfs.url, standing in for a server’s SSH dispatcher with a fake core.sshCommand that declines git-lfs-transfer (forcing the authenticate fallback) and answers git-lfs-authenticate with bytes from Authenticate.handle/3. Proves: push round-trips the object, locking is reachable (no “does not support locking”, no hang), and push-time /locks/verify succeeds even with locksverify=true.

The locking defect (#66) had the same root cause as #65; the authenticate handoff makes the endpoint discoverable, so locking works and the push completes.

Testing

  • 9 unit tests for Authenticate (ops, header passthrough, expiry, validation, telemetry).
  • 3 SSH interop tests against real git lfs 3.7 (push, lock/list/unlock, push-time verify with locksverify=true).
  • Full suite: 985 passed, 0 failures. mix format clean, credo --strict clean on all new files, anvil requirement status passes (REQ-LFS-007, REQ-LFS-008).

Consumer follow-up (not in this library PR)

The Anvil git server must wire its SSH command dispatcher to recognise git-lfs-authenticate and call Authenticate.handle/3 (docs/example in the module). Tracked separately from this library change.

🤖 Generated with Claude Code

Created Jun 15, 2026 at 20:04 UTC | Merged Jun 17, 2026 at 15:15 UTC by colechristensen cole.christensen@gmail.com