Skip to main content

is_git_ref_name

Function is_git_ref_name 

Source
pub fn is_git_ref_name(s: &str) -> Result<(), String>
Expand description

Predicate: assert that s is a valid Git ref name under the git check-ref-format --allow-onelevel rule set — the canonical shape every typed :fonte (:tipo git …) :tag / :branch value carries. The contract — modeled on the git check-ref-format grammar the Git porcelain enforces at clone/fetch/checkout time, with the multi-component requirement waived (:tag "v0.1.0" and :branch "main" are both single-component refs, the canonical leaf form for caixa’s :fonte pin axes):

  • 1..=GIT_REF_NAME_MAX_LEN (255) bytes — the POSIX NAME_MAX filesystem-component limit Git’s loose-ref .git/refs/<cat>/<name> storage tops out at;
  • no ASCII control characters (0x00..=0x1F, 0x7F) — Git’s refname parser rejects them, and the \r / \n arms are the canonical “the paste-from-doc spans multiple lines” footgun;
  • no whitespace (space, tab) — Git’s refname parser rejects them too; a :tag "v0.1.0 " (trailing space, from a copy-paste) silently passes string emptiness checks and fails at git fetch origin tag 'v0.1.0 ' with a quoting-confused error far from the source caixa.lisp;
  • no non-ASCII bytes (>= 0x80) — Git’s refname rules predate UTF-8 normalization (NFC vs NFD on APFS silently rewrites the ref body, breaking the lacre’s content addressing); the intersection-floor every realistic Git host accepts is ASCII identifiers + the small punctuation set below;
  • no ~, ^, :, ?, *, [, \ anywhere — Git reserves these for revision-grammar expressions (HEAD~3, HEAD^, :/searched, glob wildcards, refspec brackets, Windows-path backslash);
  • no @{ sequence — Git’s reflog grammar (HEAD@{2 hours ago}, branch@{upstream});
  • the bare @ is not a valid refname (it’s the alias for HEAD);
  • no .. anywhere (Git’s <rev1>..<rev2> range syntax + the . / .. parent-traversal footgun);
  • per /-separated component: must not begin with . (Git refuses to follow loose .git/refs/<cat>/.<name> files), must not end with .lock (Git’s atomic-rename guard suffix), must not be empty (// rejected by the no-empty-component arm below);
  • no leading /, no trailing /, no consecutive //;
  • no trailing . on the whole ref (Git rejects <name>.);
  • no refs/heads/ or refs/tags/ prefix — the canonical “I copied the fully-qualified ref name out of git show-ref instead of the leaf” footgun (per [theory/FLAKE-DEDUP.md][fd] BranchName constructor rules); the caixa-resolver prepends the category prefix at clone time, so an author-side :branch "refs/heads/main" resolves to a literal ref named refs/heads/refs/heads/main on disk.

Returns the parser-shaped reason on rejection (without wrapping in any error variant) so each per-axis caller — DepSource::validate for the :fonte :tag / :fonte :branch axes at validate time, the future per-pin gate on lacre.lisp resolved-ref axes, the future M4 per-dep CR materializer’s per-pin validator — wraps the same reason in its own typed *Invalid { <axis>, reason } variant. The reason wording is axis-agnostic (“git ref names reject ASCII control characters”) so every call site reading the same diagnostic points at the same rule; drift between any two axes’ rule enforcement is a build error visible at this predicate, not a per-renderer “this passed validate but git fetch rejected at clone time” surprise.

Empty input is rejected here (defensively) and at each call site via the narrower crate::DepError::FontePinEmpty variant — the same empty-first cascade is_dns_1123_label, is_gateway_api_http_path, is_wit_world_ref, is_nats_subject, and is_wasi_keyvalue_slot all carry.

:rev is intentionally NOT routed through this predicate — its author-surface shape is a hex commit-ID ([0-9a-f]+), not a refname; a dedicated is_git_oid predicate on the parallel hex-shape trajectory carries the reproducibility contract. Routing :rev through is_git_ref_name would admit :rev "main", defeating the reproducibility contract :rev carries vs. :branch / :tag. The reverse mis-slot — a canonical OID (40-char SHA-1 or 64-char SHA-256 lowercase hex) pasted into the :tag / :branch slot — is closed by this predicate too: a pre-emption arm below rejects any value whose width and byte set match the canonical OID shape, surfacing the cross-axis mis-slot at validate time with a diagnostic pointing the author at the :rev slot. The two predicates’ valid sets intersect at exactly the empty set, structurally.

Lifted as a typed substrate-side primitive on the same trajectory the peer value-shape predicates (is_dns_1123_label, is_gateway_api_http_path, is_wit_world_ref, is_nats_subject, is_wasi_keyvalue_slot) already follow — the typed slot’s valid set matches the Git porcelain’s accepted set, structurally. The sixth value-shape primitive to land in crate::render, and the first to gate a non-K8s downstream landing surface (git CLI invocation from caixa-resolver, vs. the K8s apiserver / NATS server / WASI kv backend for the prior five).

[fd]: pleme-io/theory/FLAKE-DEDUP.md §1 BranchName

§Errors

Returns the parser-shaped reason naming the specific violation (length / control-char / forbidden-char / component-shape / prefix), without wrapping in any error variant — every caller maps the same String into its own typed *Invalid { <axis>, reason } enum variant.