lfs: fix pre-commit hook silently skipping non-ASCII filenames

`git diff --cached --name-only` C-quotes filenames with non-ASCII
bytes by default (e.g. a literal "\305\202" for a Polish "ł"), which
doesn't match any real path on disk -- the hook's `[ -f "$file" ]`
check silently skipped those files instead of erroring, so they were
committed as valid LFS pointers but never pinned to IPFS. Use `-z`
(NUL-separated, unquoted) and convert to newlines for the read loop.

Also add an idempotency fast-path to `rad lfs store`/`store_object`:
if a note already exists for an object (e.g. written moments earlier
by `rad lfs precommit`), return its CID directly instead of
re-encrypting. This avoids redundant work, and matters more than that
for private repos specifically: `git push`'s custom-transfer-agent
subprocess chain has no TTY, so without this fast path every push
would fail outright on the passphrase prompt even for objects already
stored interactively at commit time.
This commit is contained in:
Maciek "mab122" Bator 2026-07-16 12:41:01 +02:00
parent c070aac6fe
commit fa4fdf5470
2 changed files with 21 additions and 1 deletions

View File

@ -82,7 +82,13 @@ const HOOK_BODY: &str = r#"rad_lfs_precommit() {
staged=$(mktemp) || return 1
batch=$(mktemp) || { rm -f "$staged"; return 1; }
git diff --cached --name-only --diff-filter=ACM > "$staged"
# `-z` avoids git's default C-style quoting of non-ASCII filenames (e.g.
# a literal "\305\202" for a Polish "ł") in `--name-only`'s normal
# output, which would otherwise not match any real file on disk and
# silently drop that file from LFS pinning. NUL-separated records are
# converted to newline-separated here for a plain `read` loop, which is
# safe since filenames practically never contain literal newlines.
git diff --cached --name-only --diff-filter=ACM -z | tr '\0' '\n' > "$staged"
while IFS= read -r file; do
[ -n "$file" ] || continue

View File

@ -53,6 +53,20 @@ pub fn store_object(
.blob(pointer_text.as_bytes())
.context("failed to write LFS pointer blob")?;
// Fast path: a note already recorded for this object (typically written
// moments ago by `rad lfs precommit`, or fetched from a peer) means the
// content is already pinned in IPFS -- nothing left to do. This matters
// beyond just avoiding duplicate work: `git push`'s custom-transfer-agent
// subprocess chain has no TTY (its stdio is consumed by the transfer
// protocol, not a terminal), so without this fast path, a private repo's
// `git push` would call back into `store_object` per file and fail
// outright on the passphrase prompt every time, even though the actual
// encryption work was already done (and the passphrase already
// supplied) once, interactively, at commit time.
if let Some(cid) = lfs_crypto::find_cids(repo, blob_oid)?.into_iter().next() {
return Ok(cid);
}
ipfs::check_daemon()?;
let envelope = if doc.visibility().is_public() {