Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
core: RepoId
Fintan Halpenny committed 3 months ago
commit 4d1a9ac2f4faced78ab388e0b9855eabaf3b38f8
parent af3f07627b05dac4a68b102e2908da23b061a631
17 files changed +549 -28
modified Cargo.lock
@@ -637,7 +637,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
 "generic-array",
-
 "rand_core",
+
 "rand_core 0.6.4",
 "subtle",
 "zeroize",
]
@@ -649,7 +649,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
 "generic-array",
-
 "rand_core",
+
 "rand_core 0.6.4",
 "typenum",
]

@@ -878,7 +878,7 @@ dependencies = [
 "generic-array",
 "group",
 "pkcs8",
-
 "rand_core",
+
 "rand_core 0.6.4",
 "sec1",
 "subtle",
 "zeroize",
@@ -1006,7 +1006,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
dependencies = [
-
 "rand_core",
+
 "rand_core 0.6.4",
 "subtle",
]

@@ -1045,6 +1045,12 @@ dependencies = [
]

[[package]]
+
name = "fnv"
+
version = "1.0.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+

+
[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1675,7 +1681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
 "ff",
-
 "rand_core",
+
 "rand_core 0.6.4",
 "subtle",
]

@@ -2347,7 +2353,7 @@ dependencies = [
 "num-integer",
 "num-iter",
 "num-traits",
-
 "rand",
+
 "rand 0.8.5",
 "smallvec",
 "zeroize",
]
@@ -2493,7 +2499,7 @@ dependencies = [
 "ecdsa",
 "elliptic-curve",
 "primeorder",
-
 "rand_core",
+
 "rand_core 0.6.4",
 "sha2",
]

@@ -2725,12 +2731,31 @@ dependencies = [
]

[[package]]
+
name = "proptest"
+
version = "1.9.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40"
+
dependencies = [
+
 "bit-set",
+
 "bit-vec",
+
 "bitflags 2.9.1",
+
 "num-traits",
+
 "rand 0.9.2",
+
 "rand_chacha 0.9.0",
+
 "rand_xorshift",
+
 "regex-syntax 0.8.5",
+
 "rusty-fork",
+
 "tempfile",
+
 "unarray",
+
]
+

+
[[package]]
name = "qcheck"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b439bd4242da51d62d18c95e6a6add749346756b0d1a587dfd0cc22fa6b5f3f0"
dependencies = [
-
 "rand",
+
 "rand 0.8.5",
]

[[package]]
@@ -2745,6 +2770,12 @@ dependencies = [
]

[[package]]
+
name = "quick-error"
+
version = "1.2.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+

+
[[package]]
name = "quick-xml"
version = "0.38.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2896,6 +2927,24 @@ dependencies = [
]

[[package]]
+
name = "radicle-core"
+
version = "0.1.0"
+
dependencies = [
+
 "git2",
+
 "gix-hash",
+
 "multibase",
+
 "proptest",
+
 "qcheck",
+
 "radicle-git-ref-format",
+
 "radicle-oid",
+
 "schemars",
+
 "serde",
+
 "serde_json",
+
 "sqlite",
+
 "thiserror 2.0.17",
+
]
+

+
[[package]]
name = "radicle-crypto"
version = "0.14.0"
dependencies = [
@@ -3158,8 +3207,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
 "libc",
-
 "rand_chacha",
-
 "rand_core",
+
 "rand_chacha 0.3.1",
+
 "rand_core 0.6.4",
+
]
+

+
[[package]]
+
name = "rand"
+
version = "0.9.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
+
dependencies = [
+
 "rand_chacha 0.9.0",
+
 "rand_core 0.9.3",
]

[[package]]
@@ -3169,7 +3228,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
 "ppv-lite86",
-
 "rand_core",
+
 "rand_core 0.6.4",
+
]
+

+
[[package]]
+
name = "rand_chacha"
+
version = "0.9.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+
dependencies = [
+
 "ppv-lite86",
+
 "rand_core 0.9.3",
]

[[package]]
@@ -3182,6 +3251,24 @@ dependencies = [
]

[[package]]
+
name = "rand_core"
+
version = "0.9.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+
dependencies = [
+
 "getrandom 0.3.3",
+
]
+

+
[[package]]
+
name = "rand_xorshift"
+
version = "0.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
+
dependencies = [
+
 "rand_core 0.9.3",
+
]
+

+
[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3300,7 +3387,7 @@ dependencies = [
 "num-traits",
 "pkcs1",
 "pkcs8",
-
 "rand_core",
+
 "rand_core 0.6.4",
 "sha2",
 "signature 2.2.0",
 "spki",
@@ -3347,6 +3434,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"

[[package]]
+
name = "rusty-fork"
+
version = "0.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
+
dependencies = [
+
 "fnv",
+
 "quick-error",
+
 "tempfile",
+
 "wait-timeout",
+
]
+

+
[[package]]
name = "ryu"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3630,7 +3729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
 "digest",
-
 "rand_core",
+
 "rand_core 0.6.4",
]

[[package]]
@@ -3789,7 +3888,7 @@ dependencies = [
 "p256",
 "p384",
 "p521",
-
 "rand_core",
+
 "rand_core 0.6.4",
 "rsa",
 "sec1",
 "sha2",
@@ -4377,6 +4476,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"

[[package]]
+
name = "unarray"
+
version = "0.1.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
+

+
[[package]]
name = "unicode-display-width"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4544,6 +4649,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"

[[package]]
+
name = "wait-timeout"
+
version = "0.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
+
dependencies = [
+
 "libc",
+
]
+

+
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4984,7 +5098,7 @@ checksum = "1ccf671d62d1bd0c913d9059e69bb4a6b51f7a4c899ab83c62d921e35f206053"
dependencies = [
 "defer-heavy",
 "log",
-
 "rand",
+
 "rand 0.8.5",
 "sync-ptr",
 "windows",
]
modified Cargo.toml
@@ -38,6 +38,7 @@ log = "0.4.17"
multibase = "0.9.1"
nonempty = "0.9.0"
pretty_assertions = "1.3.0"
+
proptest = "1.9"
qcheck = { version = "1", default-features = false }
qcheck-macros = { version = "1", default-features = false }
radicle = { version = "0.20", path = "crates/radicle" }
@@ -65,7 +66,7 @@ signature = "2.2"
snapbox = "0.4.3"
sqlite = "0.32.0"
tempfile = "3.3.0"
-
thiserror = "2"
+
thiserror = { version = "2", default-features = false }
winpipe = "0.1.1"
zeroize = "1.5.7"

modified crates/radicle-cli-test/Cargo.toml
@@ -18,4 +18,4 @@ pretty_assertions = { workspace = true }
radicle = { workspace = true, features = ["logger", "test"]}
shlex = { workspace = true }
snapbox = { workspace = true }
-
thiserror = { workspace = true }

\ No newline at end of file
+
thiserror = { workspace = true, default-features = true }

\ No newline at end of file
modified crates/radicle-cli/Cargo.toml
@@ -35,7 +35,7 @@ serde = { workspace = true }
serde_json = { workspace = true }
shlex = { workspace = true }
tempfile = { workspace = true }
-
thiserror = { workspace = true }
+
thiserror = { workspace = true, default-features = true }
timeago = { version = "0.4.2", default-features = false }
tree-sitter = "0.24.4"
tree-sitter-bash = "0.23.3"
modified crates/radicle-cob/Cargo.toml
@@ -32,7 +32,7 @@ radicle-oid = { workspace = true, features = ["git2", "serde", "std"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
signature = { workspace = true }
-
thiserror = { workspace = true }
+
thiserror = { workspace = true, default-features = true }

[dev-dependencies]
fastrand = { workspace = true }
added crates/radicle-core/Cargo.toml
@@ -0,0 +1,37 @@
+
[package]
+
name = "radicle-core"
+
description = "Radicle core data type definitions"
+
homepage.workspace = true
+
repository.workspace = true
+
version = "0.1.0"
+
edition.workspace = true
+
license.workspace = true
+
keywords = ["radicle", "git", "data types"]
+
rust-version.workspace = true
+

+
# For documentation of features refer to the module documentation in `./lib.rs`
+
[features]
+
default = ["std"]
+
git2 = ["dep:git2", "radicle-oid/git2"]
+
gix = ["dep:gix-hash", "radicle-oid/gix"]
+
std = ["radicle-oid/std", "thiserror/std", "schemars/std", "serde/std"]
+

+
[dependencies]
+
git2 = { workspace = true, optional = true }
+
gix-hash = { workspace = true, optional = true }
+
multibase = { workspace = true }
+
proptest = { workspace = true, optional = true }
+
qcheck = { workspace = true, optional = true }
+
radicle-git-ref-format = { workspace = true, optional = true }
+
radicle-oid = { workspace = true, default-features = false, features = ["sha1"] }
+
schemars = { workspace = true, optional = true, default-features = false, features = ["derive"] }
+
serde = { workspace = true, optional = true, default-features = false }
+
sqlite = { workspace = true, optional = true }
+
thiserror = { workspace = true, default-features = false }
+

+
[dev-dependencies]
+
proptest = { workspace = true }
+
serde_json = { workspace = true }
+

+
[lints]
+
workspace = true
added crates/radicle-core/src/lib.rs
@@ -0,0 +1,75 @@
+
#![no_std]
+

+
//! This a crate for defining core data type for the Radicle protocol, such as
+
//! [`RepoId`].
+
//!
+
//! # Feature Flags
+
//!
+
//! The only default feature is `std`.
+
//!
+
//! ## `std`
+
//!
+
//! [`OsString`]: ::doc_std::ffi::OsString
+
//!
+
//! Provides implementation of [`TryFrom<OsString>`].
+
//!
+
//! Enabled by default, since it is expected that most dependents will use the
+
//! standard library.
+
//!
+
//! ## `git2`
+
//!
+
//! [`git2::Oid`]: ::git2::Oid
+
//!
+
//! Provides conversion from a [`git2::Oid`] to a [`RepoId`].
+
//!
+
//! ## `gix`
+
//!
+
//! [`ObjectId`]: ::gix_hash::ObjectId
+
//!
+
//! Provides conversion from a [`ObjectId`] to a [`RepoId`].
+
//!
+
//! ## `radicle-git-ref-format`
+
//!
+
//! Provides conversions from data types defined in `radicle-core` into valid
+
//! reference components and/or strings.
+
//!
+
//! ## `serde`
+
//!
+
//! [`Serialize`]: ::serde::ser::Serialize
+
//! [`Deserialize`]: ::serde::de::Deserialize
+
//!
+
//! Provides implementations of [`Serialize`] and [`Deserialize`].
+
//!
+
//! ## `schemars`
+
//!
+
//! [`JsonSchema`]: ::schemars::JsonSchema
+
//!
+
//! Provides implementations of [`JsonSchema`].
+
//!
+
//! ## `proptest`
+
//!
+
//! [`proptest::Strategy`]: ::proptest::strategy::Strategy
+
//!
+
//! Provides functions for generating different types of [`proptest::Strategy`].
+
//!
+
//! ## `qcheck`
+
//!
+
//! [`qcheck::Arbitrary`]: ::qcheck::Arbitrary
+
//!
+
//! Provides implementations of [`qcheck::Arbitrary`].
+
//!
+
//! ## `sqlite`
+
//!
+
//! [`sqlite::BindableWithIndex`]: ::sqlite::BindableWithIndex
+
//! [`sqlite::Value`]: ::sqlite::Value
+
//!
+
//! Provides implementations of [`sqlite::BindableWithIndex`] and `TryFrom`
+
//! implementations from the [`sqlite::Value`] type to the domain type.
+

+
#[cfg(doc)]
+
extern crate std as doc_std;
+

+
extern crate alloc;
+

+
pub mod repo;
+
pub use repo::RepoId;
added crates/radicle-core/src/repo.rs
@@ -0,0 +1,294 @@
+
use alloc::string::String;
+
use alloc::string::ToString as _;
+
use alloc::vec::Vec;
+

+
use radicle_oid::Oid;
+
use thiserror::Error;
+

+
/// Radicle identifier prefix.
+
pub const RAD_PREFIX: &str = "rad:";
+

+
#[derive(Error, Debug)]
+
pub enum IdError {
+
    #[error(transparent)]
+
    Multibase(#[from] multibase::Error),
+
    #[error("invalid length: expected {expected} bytes, got {actual} bytes")]
+
    Length { expected: usize, actual: usize },
+
}
+

+
/// A repository identifier.
+
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+
pub struct RepoId(
+
    #[cfg_attr(feature = "schemars", schemars(
+
        with = "String",
+
        description = "A repository identifier. Starts with \"rad:\", followed by a multibase Base58 encoded Git object identifier.",
+
        regex(pattern = r"rad:z[1-9a-km-zA-HJ-NP-Z]+"),
+
        length(min = 5),
+
        example = &"rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
+
    ))]
+
    Oid,
+
);
+

+
impl core::fmt::Display for RepoId {
+
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+
        f.write_str(self.urn().as_str())
+
    }
+
}
+

+
impl core::fmt::Debug for RepoId {
+
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+
        write!(f, "RepoId({self})")
+
    }
+
}
+

+
impl RepoId {
+
    /// Format the identifier as a human-readable URN.
+
    ///
+
    /// Eg. `rad:z3XncAdkZjeK9mQS5Sdc4qhw98BUX`.
+
    ///
+
    pub fn urn(&self) -> String {
+
        RAD_PREFIX.to_string() + &self.canonical()
+
    }
+

+
    /// Parse an identifier from the human-readable URN format.
+
    /// Accepts strings without the radicle prefix as well,
+
    /// for convenience.
+
    pub fn from_urn(s: &str) -> Result<Self, IdError> {
+
        let s = s.strip_prefix(RAD_PREFIX).unwrap_or(s);
+
        let id = Self::from_canonical(s)?;
+

+
        Ok(id)
+
    }
+

+
    /// Format the identifier as a multibase string.
+
    ///
+
    /// Eg. `z3XncAdkZjeK9mQS5Sdc4qhw98BUX`.
+
    ///
+
    pub fn canonical(&self) -> String {
+
        multibase::encode(multibase::Base::Base58Btc, AsRef::<[u8]>::as_ref(&self.0))
+
    }
+

+
    pub fn from_canonical(input: &str) -> Result<Self, IdError> {
+
        const EXPECTED_LEN: usize = 20;
+
        let (_, bytes) = multibase::decode(input)?;
+
        let bytes: [u8; EXPECTED_LEN] =
+
            bytes.try_into().map_err(|bytes: Vec<u8>| IdError::Length {
+
                expected: EXPECTED_LEN,
+
                actual: bytes.len(),
+
            })?;
+
        Ok(Self(Oid::from_sha1(bytes)))
+
    }
+
}
+

+
impl core::str::FromStr for RepoId {
+
    type Err = IdError;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        Self::from_urn(s)
+
    }
+
}
+

+
#[cfg(feature = "std")]
+
mod std_impls {
+
    extern crate std;
+

+
    use super::{IdError, RepoId};
+

+
    use std::ffi::OsString;
+

+
    impl TryFrom<OsString> for RepoId {
+
        type Error = IdError;
+

+
        fn try_from(value: OsString) -> Result<Self, Self::Error> {
+
            let string = value.to_string_lossy();
+
            Self::from_canonical(&string)
+
        }
+
    }
+

+
    impl std::hash::Hash for RepoId {
+
        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+
            self.0.hash(state)
+
        }
+
    }
+
}
+

+
impl From<Oid> for RepoId {
+
    fn from(oid: Oid) -> Self {
+
        Self(oid)
+
    }
+
}
+

+
impl core::ops::Deref for RepoId {
+
    type Target = Oid;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
#[cfg(feature = "git2")]
+
mod git2_impls {
+
    use super::RepoId;
+

+
    impl From<git2::Oid> for RepoId {
+
        fn from(oid: git2::Oid) -> Self {
+
            Self(oid.into())
+
        }
+
    }
+
}
+

+
#[cfg(feature = "gix")]
+
mod gix_impls {
+
    use super::RepoId;
+

+
    impl From<gix_hash::ObjectId> for RepoId {
+
        fn from(oid: gix_hash::ObjectId) -> Self {
+
            Self(oid.into())
+
        }
+
    }
+
}
+

+
#[cfg(feature = "radicle-git-ref-format")]
+
mod radicle_git_ref_format_impls {
+
    use alloc::string::ToString;
+

+
    use radicle_git_ref_format::{Component, RefString};
+

+
    use super::RepoId;
+

+
    impl From<&RepoId> for Component<'_> {
+
        fn from(id: &RepoId) -> Self {
+
            let refstr = RefString::try_from(id.0.to_string())
+
                .expect("repository id's are valid ref strings");
+
            Component::from_refstr(refstr).expect("repository id's are valid refname components")
+
        }
+
    }
+
}
+

+
#[cfg(feature = "serde")]
+
mod serde_impls {
+
    use alloc::string::String;
+

+
    use serde::{de, Deserialize, Deserializer, Serialize};
+

+
    use super::RepoId;
+

+
    impl Serialize for RepoId {
+
        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
        where
+
            S: serde::Serializer,
+
        {
+
            serializer.collect_str(&self.urn())
+
        }
+
    }
+

+
    impl<'de> Deserialize<'de> for RepoId {
+
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+
        where
+
            D: Deserializer<'de>,
+
        {
+
            String::deserialize(deserializer)?
+
                .parse()
+
                .map_err(de::Error::custom)
+
        }
+
    }
+

+
    #[cfg(test)]
+
    mod test {
+
        use proptest::proptest;
+

+
        use super::super::*;
+

+
        fn prop_roundtrip_serde_json(rid: RepoId) {
+
            let encoded = serde_json::to_string(&rid).unwrap();
+
            let decoded = serde_json::from_str(&encoded).unwrap();
+

+
            assert_eq!(rid, decoded);
+
        }
+

+
        proptest! {
+
            #[test]
+
            fn assert_prop_roundtrip_serde_json(rid in arbitrary::rid()) {
+
                prop_roundtrip_serde_json(rid)
+
            }
+
        }
+
    }
+
}
+

+
#[cfg(feature = "sqlite")]
+
mod sqlite_impls {
+
    use alloc::format;
+
    use alloc::string::ToString;
+

+
    use super::RepoId;
+

+
    use sqlite::{BindableWithIndex, Error, ParameterIndex, Statement, Value};
+

+
    impl TryFrom<&Value> for RepoId {
+
        type Error = Error;
+

+
        fn try_from(value: &Value) -> Result<Self, Self::Error> {
+
            match value {
+
                Value::String(id) => RepoId::from_urn(id).map_err(|e| Error {
+
                    code: None,
+
                    message: Some(e.to_string()),
+
                }),
+
                _ => Err(Error {
+
                    code: None,
+
                    message: Some(format!("sql: invalid type `{:?}` for id", value.kind())),
+
                }),
+
            }
+
        }
+
    }
+

+
    impl BindableWithIndex for &RepoId {
+
        fn bind<I: ParameterIndex>(self, stmt: &mut Statement<'_>, i: I) -> sqlite::Result<()> {
+
            self.urn().as_str().bind(stmt, i)
+
        }
+
    }
+
}
+

+
#[cfg(any(test, feature = "proptest"))]
+
pub mod arbitrary {
+
    use proptest::prelude::Strategy;
+

+
    use super::RepoId;
+

+
    pub fn rid() -> impl Strategy<Value = RepoId> {
+
        proptest::array::uniform20(proptest::num::u8::ANY)
+
            .prop_map(|bytes| RepoId::from(radicle_oid::Oid::from_sha1(bytes)))
+
    }
+
}
+

+
#[cfg(feature = "qcheck")]
+
impl qcheck::Arbitrary for RepoId {
+
    fn arbitrary(g: &mut qcheck::Gen) -> Self {
+
        let bytes = <[u8; 20]>::arbitrary(g);
+
        let oid = radicle_oid::Oid::from_sha1(bytes);
+

+
        RepoId::from(oid)
+
    }
+
}
+

+
#[cfg(test)]
+
#[allow(clippy::unwrap_used)]
+
mod test {
+
    use super::*;
+
    use proptest::proptest;
+

+
    fn prop_roundtrip_parse(rid: RepoId) {
+
        use core::str::FromStr as _;
+
        let encoded = rid.to_string();
+
        let decoded = RepoId::from_str(&encoded).unwrap();
+

+
        assert_eq!(rid, decoded);
+
    }
+

+
    proptest! {
+
        #[test]
+
        fn assert_prop_roundtrip_parse(rid in arbitrary::rid()) {
+
            prop_roundtrip_parse(rid)
+
        }
+
    }
+
}
modified crates/radicle-crypto/Cargo.toml
@@ -29,7 +29,7 @@ serde = { workspace = true, features = ["derive", "std"] }
signature = { workspace = true }
sqlite = { workspace = true, features = ["bundled"], optional = true }
ssh-key = { version = "0.6.3", default-features = false, features = ["std", "encryption", "getrandom"], optional = true }
-
thiserror = { workspace = true }
+
thiserror = { workspace = true, default-features = true }
zeroize = { workspace = true }

[dev-dependencies]
modified crates/radicle-fetch/Cargo.toml
@@ -22,4 +22,4 @@ nonempty = { workspace = true }
radicle = { workspace = true }
radicle-oid = { workspace = true, features = ["gix"] }
radicle-git-ref-format = { workspace = true, features = ["bstr"] }
-
thiserror = { workspace = true }

\ No newline at end of file
+
thiserror = { workspace = true, default-features = true }

\ No newline at end of file
modified crates/radicle-git-metadata/Cargo.toml
@@ -10,4 +10,4 @@ keywords = ["radicle", "git", "metadata"]
rust-version.workspace = true

[dependencies]
-
thiserror = { workspace = true }

\ No newline at end of file
+
thiserror = { workspace = true, default-features = true }

\ No newline at end of file
modified crates/radicle-node/Cargo.toml
@@ -41,7 +41,7 @@ snapbox = { workspace = true, optional = true }
socket2 = { version = "0.5.7", features = ["all"], optional = true }
structured-logger = { version = "1.0.4", optional = true }
tempfile = { workspace = true }
-
thiserror = { workspace = true }
+
thiserror = { workspace = true, default-features = true }

[target.'cfg(target_os = "linux")'.dependencies]
radicle-systemd = { workspace = true, optional = true }
modified crates/radicle-protocol/Cargo.toml
@@ -27,7 +27,7 @@ sqlite = { workspace = true, features = ["bundled"] }
scrypt = { version = "0.11.0", default-features = false }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["preserve_order"] }
-
thiserror = { workspace = true }
+
thiserror = { workspace = true, default-features = true }

[dev-dependencies]
paste = "1.0.15"
modified crates/radicle-remote-helper/Cargo.toml
@@ -19,4 +19,4 @@ log = { workspace = true }
radicle = { workspace = true }
radicle-cli = { workspace = true }
radicle-crypto = { workspace = true }
-
thiserror = { workspace = true }

\ No newline at end of file
+
thiserror = { workspace = true, default-features = true }

\ No newline at end of file
modified crates/radicle-ssh/Cargo.toml
@@ -14,7 +14,7 @@ edition.workspace = true
rust-version.workspace = true

[dependencies]
-
thiserror = { workspace = true }
+
thiserror = { workspace = true, default-features = true }
zeroize = { workspace = true }

[target.'cfg(windows)'.dependencies]
modified crates/radicle-term/Cargo.toml
@@ -20,7 +20,7 @@ inquire = { version = "0.7.4", default-features = false, features = [
    "crossterm",
    "editor",
] }
-
thiserror = { workspace = true }
+
thiserror = { workspace = true, default-features = true }
unicode-display-width = "0.3.0"
unicode-segmentation = "1.7.1"
zeroize = { workspace = true }
modified crates/radicle/Cargo.toml
@@ -45,7 +45,7 @@ serde-untagged = "0.1.7"
siphasher = "1.0.0"
sqlite = { workspace = true, features = ["bundled"] }
tempfile = { workspace = true, optional = true }
-
thiserror = { workspace = true }
+
thiserror = { workspace = true, default-features = true }
unicode-normalization = { version = "0.1" }

[target.'cfg(unix)'.dependencies]