download_inner used to spawn a fresh `rad lfs fetch` subprocess for
every object, each one independently loading the signer -- for a
private repo, checking out N files meant N separate passphrase
prompts (or N failures without RAD_PASSPHRASE). Same bug class the
pre-commit hook had before it was batched into `rad lfs precommit`,
just on the download side.
Fetch can't batch the same way: git-lfs's custom-transfer protocol
requests objects one at a time, so unlike the pre-commit hook (which
knows every staged file upfront), there's no point where the full
object set is known before starting. Instead, `fetch_worker::FetchWorker`
spawns `rad lfs fetch-batch` (new, radicle-heartwood-lfs) lazily on
first use and keeps it alive for the rest of the session, one request
per download, one JSON response line back -- so the signer is loaded
(and the passphrase prompted for) at most once per session.
If the worker process itself dies (broken pipe, unexpected exit) --
as opposed to a legitimate per-object failure like a missing note or
wrong passphrase, which isn't retried -- one respawn-and-retry is
attempted before giving up.
The pre-commit hook (installed by rad-heartwood-lfs's `rad lfs init`)
no longer calls `rad lfs store` once per staged file -- it batches
every file into one `rad lfs precommit` call, so a private repo's
passphrase is only prompted for once per commit rather than once per
file. Also note the push gotcha (pushing a commit and pushing its
`refs/notes/rad-lfs` mapping are two separate `git push rad`
invocations) here too, not just in the companion repo's docs.
Found via real deployment testing on mbator.pl: the download path
reported back to git-lfs used std::env::temp_dir() (/tmp), which git-lfs
then does an atomic rename() from into .git/lfs/objects/. On systems
where /tmp and the repository are on different filesystems/mounts (the
VPS in question), that rename fails outright with "invalid cross-device
link" rather than falling back to a copy -- git-lfs doesn't retry with a
copy+delete.
The actual IPFS fetch and decryption were working correctly the whole
time (confirmed: download succeeded, byte size matched exactly) -- this
was purely the final handoff step failing.
Fix: resolve `.git/lfs/tmp` (via `git rev-parse --git-dir`, so this works
whether invoked from the repo root or a subdirectory) and download there
instead. This is git-lfs's own existing convention for its own internal
temp downloads, so reusing it guarantees same-filesystem semantics by
construction rather than hoping /tmp happens to line up.
This does reintroduce a `git` binary dependency (previously removed once
this crate stopped needing to read/write notes directly) -- an acceptable
tradeoff, since anything using `rad`/git-lfs at all already requires git
to be installed.
Private Radicle repos aren't encrypted at the object level -- access
control happens at Radicle's replication layer, which has no jurisdiction
over IPFS's own block-exchange layer. Once content is pinned and joins
the public IPFS network, anyone who obtains the CID can fetch it. A
companion change in radicle-heartwood-lfs adds client-side envelope
encryption for private repos, via two new plumbing commands: `rad lfs
store` and `rad lfs fetch`.
This crate now shells out to those instead of talking to Kubo/git-notes
directly: upload spawns `rad lfs store`, download spawns `rad lfs fetch`.
Removed src/notes.rs entirely (no longer needed -- the Radicle-side
commands own all of that, including note reads/writes) and trimmed
kubo.rs down to just the init-event healthcheck. This crate still has no
dependency on Radicle's identity/crypto stack, and no longer needs `git`
on PATH at all -- only `rad` and a local IPFS daemon.
Implements the Git LFS custom-transfer-agent protocol (init/upload/download/
terminate over stdin/stdout JSON), storing large file content in a local
IPFS (Kubo) daemon rather than a central server. The oid to CID mapping is
carried in git notes on refs/notes/rad-lfs, keyed by each LFS pointer's own
git blob hash, so it replicates with the repository via normal git sync.
Companion to the rad lfs init / seed / unseed support in the paired
heartwood fork.