Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
radicle-core
Merged fintohaps opened 4 months ago

The main goal of this patch is to define core Radicle types so that crates do not have to use radicle as the main dependency, when all they need are a few core types.

This will eventually result in smaller crates that are scoped to well-defined concepts, eventually resulting in untangling the radicle crate, so that it simply re-exports all the smaller crates.

The first two types in this crate are RepoId and NodeId, since they are ubuiquitous to all other functionality.

22 files changed +593 -68 af3f0762 02318f19
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"
@@ -2794,6 +2825,7 @@ dependencies = [
 "qcheck",
 "qcheck-macros",
 "radicle-cob",
+
 "radicle-core",
 "radicle-crypto",
 "radicle-git-metadata",
 "radicle-git-ref-format",
@@ -2896,6 +2928,25 @@ dependencies = [
]

[[package]]
+
name = "radicle-core"
+
version = "0.1.0"
+
dependencies = [
+
 "git2",
+
 "gix-hash",
+
 "multibase",
+
 "proptest",
+
 "qcheck",
+
 "radicle-crypto",
+
 "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 +3209,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 +3230,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 +3253,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 +3389,7 @@ dependencies = [
 "num-traits",
 "pkcs1",
 "pkcs8",
-
 "rand_core",
+
 "rand_core 0.6.4",
 "sha2",
 "signature 2.2.0",
 "spki",
@@ -3347,6 +3436,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 +3731,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 +3890,7 @@ dependencies = [
 "p256",
 "p384",
 "p521",
-
 "rand_core",
+
 "rand_core 0.6.4",
 "rsa",
 "sec1",
 "sha2",
@@ -4377,6 +4478,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 +4651,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 +5100,7 @@ checksum = "1ccf671d62d1bd0c913d9059e69bb4a6b51f7a4c899ab83c62d921e35f206053"
dependencies = [
 "defer-heavy",
 "log",
-
 "rand",
+
 "rand 0.8.5",
 "sync-ptr",
 "windows",
]
modified Cargo.toml
@@ -38,12 +38,14 @@ 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" }
radicle-cli = { version = "0.17", path = "crates/radicle-cli" }
radicle-cli-test = { path = "crates/radicle-cli-test" }
radicle-cob = { version = "0.17", path = "crates/radicle-cob" }
+
radicle-core = { version = "0.1", path = "crates/radicle-core" }
radicle-crypto = { version = "0.14", path = "crates/radicle-crypto" }
radicle-dag = { version = "0.10", path = "crates/radicle-dag" }
radicle-fetch = { version = "0.16", path = "crates/radicle-fetch" }
@@ -65,7 +67,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,38 @@
+
[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-crypto = { workspace = 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,78 @@
+
#![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 node;
+
pub use node::NodeId;
+

+
pub mod repo;
+
pub use repo::RepoId;
added crates/radicle-core/src/node.rs
@@ -0,0 +1,24 @@
+
//! A Radicle node on the network is identified by its [`NodeId`], which in turn
+
//! is a Ed25519 public key.
+
//!
+
//! The human-readable format is a multibase-encoded format of the underlying Ed25519 public key, i.e.
+
//! ```
+
//! MULTIBASE(base58-btc, MULTICODEC(public-key-type, raw-public-key-bytes))
+
//! ```
+
//! which results in strings that look like:
+
//! ```
+
//! z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
//! ```
+

+
use radicle_crypto::PublicKey;
+

+
/// Public identifier of a node device in the network.
+
///
+
/// # Legacy
+
///
+
/// This is a type alias, providing little protection around evolving a [`NodeId`]
+
/// and having it very tightly coupled with a [`PublicKey`].
+
///
+
/// Future iterations will change this to provide a better API for working with
+
/// [`NodeId`]'s and their usage in the protocol.
+
pub type NodeId = PublicKey;
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
@@ -13,7 +13,16 @@ rust-version.workspace = true
default = []
test = ["tempfile", "qcheck", "radicle-crypto/test", "radicle-cob/test"]
logger = ["colored", "chrono"]
-
schemars = ["radicle-oid/schemars", "radicle-localtime/schemars", "dep:schemars"]
+
qcheck = [
+
  "radicle-core/qcheck",
+
  "dep:qcheck"
+
]
+
schemars = [
+
  "radicle-oid/schemars",
+
  "radicle-core/schemars",
+
  "radicle-localtime/schemars",
+
  "dep:schemars"
+
]

[dependencies]
amplify = { workspace = true, features = ["std"] }
@@ -33,6 +42,7 @@ multibase = { workspace = true }
nonempty = { workspace = true, features = ["serialize"] }
qcheck = { workspace = true, optional = true }
radicle-cob = { workspace = true, features = ["git2"] }
+
radicle-core = { workspace = true, features = ["git2", "serde", "sqlite"] }
radicle-crypto = { workspace = true, features = ["git-ref-format-core", "ssh", "sqlite", "cyphernet"] }
radicle-git-ref-format = { workspace = true, features = ["macro", "serde"] }
radicle-localtime = { workspace = true, features = ["serde"] }
@@ -45,7 +55,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]
modified crates/radicle/src/identity/doc.rs
@@ -1,7 +1,5 @@
pub mod update;

-
mod id;
-

use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::num::{NonZeroU32, NonZeroUsize};
@@ -29,7 +27,7 @@ use crate::storage;
use crate::storage::{ReadRepository, RepositoryError};

pub use crypto::PublicKey;
-
pub use id::*;
+
pub use radicle_core::repo::*;

use super::crefs::{self, RawCanonicalRefs};
use super::CanonicalRefs;
modified crates/radicle/src/node.rs
@@ -50,6 +50,7 @@ pub use cyphernet::addr::{HostName, PeerAddr, PeerAddrParseError};
pub use db::Database;
pub use events::{Event, Events};
pub use features::Features;
+
pub use radicle_core::NodeId;
pub use seed::SyncedAt;
pub use timestamp::Timestamp;

@@ -1002,9 +1003,6 @@ impl<T: DeserializeOwned> Iterator for LineIter<T> {
    }
}

-
/// Public node & device identifier.
-
pub type NodeId = PublicKey;
-

/// Node controller.
#[derive(Debug, Clone)]
pub struct Node {
modified crates/radicle/src/sql.rs
@@ -4,7 +4,6 @@ use std::str::FromStr;
use sqlite as sql;
use sqlite::Value;

-
use crate::identity::RepoId;
use crate::node;
use crate::node::{Address, UserAgent};

@@ -28,29 +27,6 @@ pub fn transaction<T, E: From<sql::Error>>(
    }
}

-
impl TryFrom<&Value> for RepoId {
-
    type Error = sql::Error;
-

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

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

impl sql::BindableWithIndex for node::Features {
    fn bind<I: sql::ParameterIndex>(self, stmt: &mut sql::Statement<'_>, i: I) -> sql::Result<()> {
        (*self.deref() as i64).bind(stmt, i)
modified crates/radicle/src/test/arbitrary.rs
@@ -270,15 +270,6 @@ impl Arbitrary for storage::Remote<crypto::Unverified> {
    }
}

-
impl Arbitrary for RepoId {
-
    fn arbitrary(g: &mut qcheck::Gen) -> Self {
-
        let bytes = <[u8; 20]>::arbitrary(g);
-
        let oid = crate::git::Oid::from_sha1(bytes);
-

-
        RepoId::from(oid)
-
    }
-
}
-

impl Arbitrary for AddressType {
    fn arbitrary(g: &mut qcheck::Gen) -> Self {
        let t = *g.choose(&[1, 2, 3, 4]).unwrap() as u8;