From 3fcf2a86da18fc958d79fd8f435e7c7edcbd2c58 Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Sun, 5 Oct 2025 11:53:21 +0200 Subject: [PATCH] git-ref-format: New crate For further details on the crate see its documentation in `crates/radicle-git-ref-format/src/lib.rs`. --- Cargo.lock | 7 + crates/radicle-git-ref-format/Cargo.toml | 18 ++ crates/radicle-git-ref-format/src/lib.rs | 298 +++++++++++++++++++++++ 3 files changed, 323 insertions(+) create mode 100644 crates/radicle-git-ref-format/Cargo.toml create mode 100644 crates/radicle-git-ref-format/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 7db4a9b9..77c0662b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2914,6 +2914,13 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "radicle-git-ref-format" +version = "0.1.0" +dependencies = [ + "git-ref-format-core", +] + [[package]] name = "radicle-node" version = "0.16.0" diff --git a/crates/radicle-git-ref-format/Cargo.toml b/crates/radicle-git-ref-format/Cargo.toml new file mode 100644 index 00000000..8a00a7c3 --- /dev/null +++ b/crates/radicle-git-ref-format/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "radicle-git-ref-format" +description = "Radicle re-exports and macros for `git-ref-format-core`" +homepage.workspace = true +repository.workspace = true +version = "0.1.0" +edition.workspace = true +license.workspace = true +keywords = ["radicle", "git", "refname", "ref", "references"] +rust-version.workspace = true + +[features] +macro = [] +bstr = ["git-ref-format-core/bstr"] +serde = ["git-ref-format-core/serde"] + +[dependencies] +git-ref-format-core.workspace = true diff --git a/crates/radicle-git-ref-format/src/lib.rs b/crates/radicle-git-ref-format/src/lib.rs new file mode 100644 index 00000000..e7df4b25 --- /dev/null +++ b/crates/radicle-git-ref-format/src/lib.rs @@ -0,0 +1,298 @@ +#![no_std] + +//! [`git_ref_format`]: https://crates.io/crates/git-ref-format +//! [`radicle-git-ext`]: https://crates.io/crates/radicle-git-ext +//! +//! This crate depends on and re-exports from [`git_ref_format_core`]. +//! +//! ## Macros +//! +//! Instead of providing procedural macros, like [`git_ref_format`] +//! it just provides much simpler declarative macros, guarded by the feature +//! flag `macro`. +//! +//! ### Benefits +//! +//! - Does not depend on [`radicle-git-ext`]. +//! - Does not pull in procedural macro dependencies. +//! - Has much smaller compile-time overhead than [`git_ref_format`]. +//! +//! ### Drawback +//! +//! The main drawback is that the macros in this crate cannot provide compile +//! time validation of the argument. Thus, these macros must be used in +//! conjunction with testing: If all generated objects are used in tests, and +//! these tests are run, then the guarantees are equally strong. Consumers that +//! do not or cannot test their code should not use the macros then. + +pub use git_ref_format_core::*; + +/// Create a [`git_ref_format_core::RefString`] from a string literal. +/// +/// Similar to [`core::debug_assert`], an optimized build will not validate +/// (but rather perform an unsafe conversion) unless `-C debug-assertions` is +/// passed to the compiler. +#[cfg(any(feature = "macro", test))] +#[macro_export] +macro_rules! refname { + ($arg:literal) => {{ + use $crate::RefString; + + #[cfg(debug_assertions)] + { + RefString::try_from($arg).expect(core::concat!( + "literal `", + $arg, + "` must be a valid reference name" + )) + } + + #[cfg(not(debug_assertions))] + { + extern crate alloc; + + use alloc::string::String; + + let s: String = $arg.to_owned(); + unsafe { core::mem::transmute::<_, RefString>(s) } + } + }}; +} + +/// Create a [`git_ref_format_core::Qualified`] from a string literal. +/// +/// Similar to [`core::debug_assert`], an optimized build will not validate +/// (but rather perform an unsafe conversion) unless `-C debug-assertions` is +/// passed to the compiler. +#[cfg(any(feature = "macro", test))] +#[macro_export] +macro_rules! qualified { + ($arg:literal) => {{ + use $crate::Qualified; + + #[cfg(debug_assertions)] + { + Qualified::from_refstr($crate::refname!($arg)).expect(core::concat!( + "literal `", + $arg, + "` must be of the form 'refs//'" + )) + } + + #[cfg(not(debug_assertions))] + { + extern crate alloc; + + use core::mem::transmute; + + use alloc::borrow::Cow; + use alloc::string::String; + + use $crate::{RefStr, RefString}; + + let s: String = $arg.to_owned(); + let refstring: RefString = unsafe { transmute(s) }; + let cow: Cow<'_, RefStr> = Cow::Owned(refstring); + let qualified: Qualified = unsafe { transmute(cow) }; + + qualified + } + }}; +} + +/// Create a [`git_ref_format_core::Component`] from a string literal. +/// +/// Similar to [`core::debug_assert`], an optimized build will not validate +/// (but rather perform an unsafe conversion) unless `-C debug-assertions` is +/// passed to the compiler. +#[cfg(any(feature = "macro", test))] +#[macro_export] +macro_rules! component { + ($arg:literal) => {{ + use $crate::Component; + + #[cfg(debug_assertions)] + { + Component::from_refstr($crate::refname!($arg)).expect(core::concat!( + "literal `", + $arg, + "` must be a valid component (cannot contain '/')" + )) + } + + #[cfg(not(debug_assertions))] + { + extern crate alloc; + + use core::mem::transmute; + + use alloc::borrow::Cow; + use alloc::string::String; + + use $crate::{RefStr, RefString}; + + let s: String = $arg.to_owned(); + let refstring: RefString = unsafe { transmute(s) }; + let cow: Cow<'_, RefStr> = Cow::Owned(refstring); + let component: Component = unsafe { transmute(cow) }; + + component + } + }}; +} + +/// Create a [`git_ref_format_core::refspec::PatternString`] from a string literal. +/// +/// Similar to [`core::debug_assert`], an optimized build will not validate +/// (but rather perform an unsafe conversion) unless `-C debug-assertions` is +/// passed to the compiler. +#[cfg(any(feature = "macro", test))] +#[macro_export] +macro_rules! pattern { + ($arg:literal) => {{ + use $crate::refspec::PatternString; + + #[cfg(debug_assertions)] + { + PatternString::try_from($arg).expect(core::concat!( + "literal `", + $arg, + "` must be a valid refspec pattern" + )) + } + + #[cfg(not(debug_assertions))] + { + extern crate alloc; + + use alloc::string::String; + + let s: String = $arg.to_owned(); + unsafe { core::mem::transmute::<_, PatternString>(s) } + } + }}; +} + +/// Create a [`git_ref_format_core::refspec::QualifiedPattern`] from a string literal. +/// +/// Similar to [`core::debug_assert`], an optimized build will not validate +/// (but rather perform an unsafe conversion) unless `-C debug-assertions` is +/// passed to the compiler. +#[cfg(any(feature = "macro", test))] +#[macro_export] +macro_rules! qualified_pattern { + ($arg:literal) => {{ + use $crate::refspec::QualifiedPattern; + + #[cfg(debug_assertions)] + { + use core::concat; + + use $crate::refspec::PatternStr; + + let pattern = PatternStr::try_from_str($arg).expect(concat!( + "literal `", + $arg, + "` must be a valid refspec pattern" + )); + + QualifiedPattern::from_patternstr(pattern).expect(concat!( + "literal `", + $arg, + "` must be a valid qualified refspec pattern" + )) + } + + #[cfg(not(debug_assertions))] + { + extern crate alloc; + + use core::mem::transmute; + + use alloc::borrow::Cow; + use alloc::string::String; + + use $crate::refspec::{PatternStr, PatternString}; + + let s: String = $arg.to_owned(); + let pattern: PatternString = unsafe { transmute(s) }; + let cow: Cow<'_, PatternStr> = Cow::Owned(pattern); + let qualified: QualifiedPattern = unsafe { transmute(cow) }; + + qualified + } + }}; +} + +#[cfg(test)] +mod test { + #[test] + fn refname() { + let _ = crate::refname!("refs/heads/main"); + let _ = crate::refname!("refs/tags/v1.0.0"); + let _ = crate::refname!("refs/remotes/origin/main"); + let _ = crate::refname!("a"); + } + + #[test] + #[should_panic] + fn refname_invalid() { + let _ = crate::refname!("a~b"); + } + + #[test] + fn qualified() { + let _ = crate::qualified!("refs/heads/main"); + let _ = crate::qualified!("refs/tags/v1.0.0"); + let _ = crate::qualified!("refs/remotes/origin/main"); + } + + #[test] + #[should_panic] + fn qualified_invalid() { + let _ = crate::qualified!("a"); + } + + #[test] + fn component() { + let _ = crate::component!("a"); + } + + #[test] + #[should_panic] + fn component_invalid() { + let _ = crate::component!("a/b"); + } + + #[test] + fn pattern() { + let _ = crate::pattern!("refs/heads/main"); + let _ = crate::pattern!("refs/tags/v1.0.0"); + let _ = crate::pattern!("refs/remotes/origin/main"); + + let _ = crate::pattern!("a"); + let _ = crate::pattern!("a/*"); + let _ = crate::pattern!("*"); + let _ = crate::pattern!("a/b*"); + let _ = crate::pattern!("a/b*/c"); + let _ = crate::pattern!("a/*/c"); + } + + #[test] + fn qualified_pattern() { + let _ = crate::qualified_pattern!("refs/heads/main"); + let _ = crate::qualified_pattern!("refs/tags/v1.0.0"); + let _ = crate::qualified_pattern!("refs/remotes/origin/main"); + + let _ = crate::qualified_pattern!("refs/heads/main/*"); + let _ = crate::qualified_pattern!("refs/tags/v*"); + let _ = crate::qualified_pattern!("refs/remotes/origin/main"); + let _ = crate::qualified_pattern!("refs/remotes/origin/department/*/person"); + } + + #[test] + #[should_panic] + fn qualified_pattern_invalid() { + let _ = crate::qualified_pattern!("a/*/b"); + } +}