From fa4fdf54704a7a77d4a19910eb4dc85dd520dcb5 Mon Sep 17 00:00:00 2001 From: "Maciek \"mab122\" Bator" Date: Thu, 16 Jul 2026 12:41:01 +0200 Subject: [PATCH] lfs: fix pre-commit hook silently skipping non-ASCII filenames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- crates/radicle-cli/src/commands/lfs/init.rs | 8 +++++++- crates/radicle-cli/src/commands/lfs/store.rs | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/radicle-cli/src/commands/lfs/init.rs b/crates/radicle-cli/src/commands/lfs/init.rs index 76798810..a703290d 100644 --- a/crates/radicle-cli/src/commands/lfs/init.rs +++ b/crates/radicle-cli/src/commands/lfs/init.rs @@ -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 diff --git a/crates/radicle-cli/src/commands/lfs/store.rs b/crates/radicle-cli/src/commands/lfs/store.rs index b10ebc10..73670536 100644 --- a/crates/radicle-cli/src/commands/lfs/store.rs +++ b/crates/radicle-cli/src/commands/lfs/store.rs @@ -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() {