Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cob: Get crate to compile without `git2`
Lorenz Leutgeb committed 7 months ago
commit 8aa0a42a9fea64d6fada6e1943c21d7220b03880
parent 853be107bf45d77139d7f20c198c6be231203b4c
21 files changed +440 -398
modified crates/radicle-cob/Cargo.toml
@@ -17,10 +17,11 @@ rust-version.workspace = true
default = []
# Only used for testing. Ensures that commit ids are stable.
stable-commit-ids = []
+
test = []

[dependencies]
fastrand = { workspace = true }
-
git2 = { workspace = true, features = ["vendored-libgit2"] }
+
git2 = { workspace = true, optional = true, features = ["vendored-libgit2"] }
log = { workspace = true }
nonempty = { workspace = true, features = ["serialize"] }
radicle-crypto = { workspace = true, features = ["ssh"] }
modified crates/radicle-cob/src/backend.rs
@@ -1,3 +1,7 @@
// Copyright © 2022 The Radicle Link Contributors

+
#[cfg(feature = "git2")]
pub mod git;
+

+
#[cfg(feature = "stable-commit-ids")]
+
pub mod stable;
modified crates/radicle-cob/src/backend/git.rs
@@ -2,9 +2,6 @@

pub mod change;

-
#[cfg(feature = "stable-commit-ids")]
-
pub mod stable;
-

/// Environment variable to set to overwrite the commit date for both the author and the committer.
///
/// The format must be a unix timestamp.
modified crates/radicle-cob/src/backend/git/change.rs
@@ -298,7 +298,7 @@ fn write_commit(
    #[cfg(feature = "stable-commit-ids")]
    // Ensures the commit id doesn't change on every run.
    let (author, timestamp) = {
-
        let stable = crate::git::stable::read_timestamp();
+
        let stable = crate::stable::read_timestamp();
        (
            Author {
                time: git_ext::author::Time::new(stable, 0),
@@ -307,11 +307,11 @@ fn write_commit(
            stable,
        )
    };
-
    let (author, timestamp) = if let Ok(s) = std::env::var(crate::git::GIT_COMMITTER_DATE) {
+
    let (author, timestamp) = if let Ok(s) = std::env::var(super::GIT_COMMITTER_DATE) {
        let Ok(timestamp) = s.trim().parse::<i64>() else {
            panic!(
                "Invalid timestamp value {s:?} for `{}`",
-
                crate::git::GIT_COMMITTER_DATE
+
                super::GIT_COMMITTER_DATE
            );
        };
        let author = Author {
deleted crates/radicle-cob/src/backend/git/stable.rs
@@ -1,79 +0,0 @@
-
use std::{cell::Cell, ops::Add};
-

-
thread_local! {
-
    /// The constant time used by the stable-commit-ids feature.
-
    pub static STABLE_TIME: Cell<i64> = const { Cell::new(1514817556) };
-
    /// An incrementing counter to advance the `STABLE_TIME` value with in
-
    /// [`with_advanced_timestamp`].
-
    pub static STEP: Cell<Step> = Cell::new(Step::default());
-
}
-

-
#[derive(Clone, Copy)]
-
struct Step(i64);
-

-
impl Default for Step {
-
    fn default() -> Self {
-
        Self(1)
-
    }
-
}
-

-
impl Add<Step> for i64 {
-
    type Output = i64;
-

-
    fn add(self, rhs: Step) -> Self::Output {
-
        self + rhs.0
-
    }
-
}
-

-
impl Add<i64> for Step {
-
    type Output = Step;
-

-
    fn add(self, rhs: i64) -> Self::Output {
-
        Step(self.0 + rhs)
-
    }
-
}
-

-
/// Read the current value of `STABLE_TIME`.
-
///
-
/// # Panics
-
///
-
/// The `STABLE_TIME` is declared in `thread_local`, and so the panic
-
/// information is repeated here.
-
///
-
/// Panics if the key currently has its destructor running, and it may panic if
-
/// the destructor has previously been run for this thread.
-
#[allow(clippy::unwrap_used)]
-
pub fn read_timestamp() -> i64 {
-
    STABLE_TIME.get()
-
}
-

-
/// Perform an action `f` that would rely on the `STABLE_TIME` value. This will
-
/// advance the `STABLE_TIME` by an increment of `1` for each time it is called,
-
/// within the same thread.
-
///
-
/// # Usage
-
///
-
/// ```rust, ignore
-
/// let oid1 = with_advanced_timestamp(|| cob.update("New revision OID"));
-
/// let oid2 = with_advanced_timestamp(|| cob.update("Another revision OID"));
-
/// ```
-
///
-
/// # Panics
-
///
-
/// The `STABLE_TIME` is declared in `thread_local`, and so the panic
-
/// information is repeated here.
-
///
-
/// Panics if the key currently has its destructor running, and it may panic if
-
/// the destructor has previously been run for this thread.
-
#[allow(clippy::unwrap_used)]
-
pub fn with_advanced_timestamp<F, T>(f: F) -> T
-
where
-
    F: FnOnce() -> T,
-
{
-
    let step = STEP.get();
-
    let original = read_timestamp();
-
    STABLE_TIME.replace(original + step);
-
    let result = f();
-
    STEP.replace(step + 1);
-
    result
-
}
added crates/radicle-cob/src/backend/stable.rs
@@ -0,0 +1,79 @@
+
use std::{cell::Cell, ops::Add};
+

+
thread_local! {
+
    /// The constant time used by the stable-commit-ids feature.
+
    pub static STABLE_TIME: Cell<i64> = const { Cell::new(1514817556) };
+
    /// An incrementing counter to advance the `STABLE_TIME` value with in
+
    /// [`with_advanced_timestamp`].
+
    pub static STEP: Cell<Step> = Cell::new(Step::default());
+
}
+

+
#[derive(Clone, Copy)]
+
struct Step(i64);
+

+
impl Default for Step {
+
    fn default() -> Self {
+
        Self(1)
+
    }
+
}
+

+
impl Add<Step> for i64 {
+
    type Output = i64;
+

+
    fn add(self, rhs: Step) -> Self::Output {
+
        self + rhs.0
+
    }
+
}
+

+
impl Add<i64> for Step {
+
    type Output = Step;
+

+
    fn add(self, rhs: i64) -> Self::Output {
+
        Step(self.0 + rhs)
+
    }
+
}
+

+
/// Read the current value of `STABLE_TIME`.
+
///
+
/// # Panics
+
///
+
/// The `STABLE_TIME` is declared in `thread_local`, and so the panic
+
/// information is repeated here.
+
///
+
/// Panics if the key currently has its destructor running, and it may panic if
+
/// the destructor has previously been run for this thread.
+
#[allow(clippy::unwrap_used)]
+
pub fn read_timestamp() -> i64 {
+
    STABLE_TIME.get()
+
}
+

+
/// Perform an action `f` that would rely on the `STABLE_TIME` value. This will
+
/// advance the `STABLE_TIME` by an increment of `1` for each time it is called,
+
/// within the same thread.
+
///
+
/// # Usage
+
///
+
/// ```rust, ignore
+
/// let oid1 = with_advanced_timestamp(|| cob.update("New revision OID"));
+
/// let oid2 = with_advanced_timestamp(|| cob.update("Another revision OID"));
+
/// ```
+
///
+
/// # Panics
+
///
+
/// The `STABLE_TIME` is declared in `thread_local`, and so the panic
+
/// information is repeated here.
+
///
+
/// Panics if the key currently has its destructor running, and it may panic if
+
/// the destructor has previously been run for this thread.
+
#[allow(clippy::unwrap_used)]
+
pub fn with_advanced_timestamp<F, T>(f: F) -> T
+
where
+
    F: FnOnce() -> T,
+
{
+
    let step = STEP.get();
+
    let original = read_timestamp();
+
    STABLE_TIME.replace(original + step);
+
    let result = f();
+
    STEP.replace(step + 1);
+
    result
+
}
modified crates/radicle-cob/src/change/store.rs
@@ -6,11 +6,12 @@ use nonempty::NonEmpty;
use oid::Oid;
use serde::{Deserialize, Serialize};

+
use crate::object::collaboration::error::{Create, Update};
use crate::{signatures, TypeName};

/// Change entry storage.
pub trait Storage {
-
    type StoreError: Error + Send + Sync + 'static;
+
    type StoreError: Error + Send + Sync + 'static + Into<Create> + Into<Update>;
    type LoadError: Error + Send + Sync + 'static;

    type ObjectId;
@@ -194,6 +195,7 @@ pub struct Embed<T = Vec<u8>> {
    pub content: T,
}

+
#[cfg(feature = "git2")]
impl<T: From<Oid>> Embed<T> {
    /// Create a new embed.
    pub fn store(
@@ -210,6 +212,7 @@ impl<T: From<Oid>> Embed<T> {
    }
}

+
#[cfg(feature = "git2")]
impl Embed<Vec<u8>> {
    /// Get the object id of the embedded content.
    pub fn oid(&self) -> Oid {
modified crates/radicle-cob/src/lib.rs
@@ -64,8 +64,13 @@ extern crate radicle_git_ext as git_ext;
extern crate radicle_oid as oid;

mod backend;
+

+
#[cfg(all(any(test, feature = "test"), feature = "git2"))]
pub use backend::git;

+
#[cfg(feature = "stable-commit-ids")]
+
pub use backend::stable;
+

mod change_graph;
mod trailers;

@@ -106,19 +111,9 @@ mod tests;
///
///   * [`object::Storage`]
///
-
/// **Note**: [`change::Storage`] is already implemented for
-
/// [`git2::Repository`]. It is expected that the underlying storage
-
/// for `object::Storage` will also be `git2::Repository`, but if not
-
/// please open an issue to change the definition of `Store` :)
pub trait Store
where
    Self: object::Storage
-
        + change::Storage<
-
            StoreError = git::change::error::Create,
-
            LoadError = git::change::error::Load,
-
            ObjectId = oid::Oid,
-
            Parent = oid::Oid,
-
            Signatures = ExtendedSignature,
-
        >,
+
        + change::Storage<ObjectId = oid::Oid, Parent = oid::Oid, Signatures = ExtendedSignature>,
{
}
modified crates/radicle-cob/src/object.rs
@@ -48,12 +48,14 @@ impl From<&Oid> for ObjectId {
    }
}

+
#[cfg(feature = "git2")]
impl From<git2::Oid> for ObjectId {
    fn from(oid: git2::Oid) -> Self {
        Oid::from(oid).into()
    }
}

+
#[cfg(feature = "git2")]
impl From<&git2::Oid> for ObjectId {
    fn from(oid: &git2::Oid) -> Self {
        ObjectId(Oid::from(*oid))
modified crates/radicle-cob/src/object/collaboration/create.rs
@@ -4,7 +4,6 @@ use nonempty::NonEmpty;

use crate::Embed;
use crate::Evaluate;
-
use crate::Store;

use super::*;

@@ -57,19 +56,24 @@ pub fn create<T, S, G>(
    signer: &G,
    resource: Option<Oid>,
    related: Vec<Oid>,
-
    identifier: &S::Namespace,
+
    identifier: &<S as crate::object::Storage>::Namespace,
    args: Create,
) -> Result<CollaborativeObject<T>, error::Create>
where
    T: Evaluate<S>,
-
    S: Store,
+
    S: crate::object::Storage
+
        + crate::change::Storage<
+
            ObjectId = crate::object::Oid,
+
            Parent = crate::object::Oid,
+
            Signatures = crate::ExtendedSignature,
+
        >,
    G: signature::Signer<crate::ExtendedSignature>,
{
    let type_name = args.type_name.clone();
    let version = args.version;
    let init_change = storage
        .store(resource, related, signer, args.template())
-
        .map_err(error::Create::from)?;
+
        .map_err(Into::<error::Create>::into)?;
    let object_id = init_change.id().into();
    let object = T::init(&init_change, storage).map_err(error::Create::evaluate)?;

modified crates/radicle-cob/src/object/collaboration/error.rs
@@ -2,14 +2,13 @@

use thiserror::Error;

-
use crate::git;
-

#[derive(Debug, Error)]
pub enum Create {
    #[error(transparent)]
    Evaluate(Box<dyn std::error::Error + Send + Sync + 'static>),
    #[error(transparent)]
-
    CreateChange(#[from] git::change::error::Create),
+
    #[cfg(feature = "git2")]
+
    CreateChange(#[from] crate::backend::git::change::error::Create),
    #[error("failed to updated references for during object creation: {err}")]
    Refs {
        #[source]
@@ -39,6 +38,7 @@ pub enum Retrieve {
    #[error(transparent)]
    Evaluate(Box<dyn std::error::Error + Send + Sync + 'static>),
    #[error(transparent)]
+
    #[cfg(feature = "git2")]
    Git(#[from] git2::Error),
    #[error("failed to get references during object retrieval: {err}")]
    Refs {
@@ -62,13 +62,15 @@ pub enum Update {
    #[error("no object found")]
    NoSuchObject,
    #[error(transparent)]
-
    CreateChange(#[from] git::change::error::Create),
+
    #[cfg(feature = "git2")]
+
    CreateChange(#[from] crate::backend::git::change::error::Create),
    #[error("failed to get references during object update: {err}")]
    Refs {
        #[source]
        err: Box<dyn std::error::Error + Send + Sync + 'static>,
    },
    #[error(transparent)]
+
    #[cfg(feature = "git2")]
    Git(#[from] git2::Error),
    #[error(transparent)]
    Io(#[from] std::io::Error),
modified crates/radicle-cob/src/object/collaboration/get.rs
@@ -1,6 +1,8 @@
// Copyright © 2022 The Radicle Link Contributors

-
use crate::{change_graph::ChangeGraph, CollaborativeObject, Evaluate, ObjectId, Store, TypeName};
+
use crypto::ssh::ExtendedSignature;
+

+
use crate::{change_graph::ChangeGraph, CollaborativeObject, Evaluate, ObjectId, TypeName};

use super::error;

@@ -20,7 +22,12 @@ pub fn get<T, S>(
) -> Result<Option<CollaborativeObject<T>>, error::Retrieve>
where
    T: Evaluate<S>,
-
    S: Store,
+
    S: crate::object::Storage,
+
    S: crate::change::Storage<
+
        ObjectId = crate::object::Oid,
+
        Parent = crate::object::Oid,
+
        Signatures = ExtendedSignature,
+
    >,
{
    let tip_refs = storage
        .objects(typename, oid)
modified crates/radicle-cob/src/object/collaboration/info.rs
@@ -6,9 +6,10 @@

use std::collections::BTreeSet;

+
use crypto::ssh::ExtendedSignature;
use oid::Oid;

-
use crate::{change_graph::ChangeGraph, ObjectId, Store, TypeName};
+
use crate::{change_graph::ChangeGraph, ObjectId, TypeName};

use super::error;

@@ -38,7 +39,12 @@ pub fn changegraph<S>(
    oid: &ObjectId,
) -> Result<Option<ChangeGraphInfo>, error::Retrieve>
where
-
    S: Store,
+
    S: crate::object::Storage,
+
    S: crate::change::Storage<
+
        ObjectId = crate::object::Oid,
+
        Parent = crate::object::Oid,
+
        Signatures = ExtendedSignature,
+
    >,
{
    let tip_refs = storage
        .objects(typename, oid)
modified crates/radicle-cob/src/object/collaboration/list.rs
@@ -1,6 +1,6 @@
// Copyright © 2022 The Radicle Link Contributors

-
use crate::{change_graph::ChangeGraph, CollaborativeObject, Evaluate, Store, TypeName};
+
use crate::{change_graph::ChangeGraph, CollaborativeObject, Evaluate, TypeName};

use super::error;

@@ -17,7 +17,12 @@ pub fn list<T, S>(
) -> Result<Vec<CollaborativeObject<T>>, error::Retrieve>
where
    T: Evaluate<S>,
-
    S: Store,
+
    S: crate::object::Storage,
+
    S: crate::change::Storage<
+
        ObjectId = crate::object::Oid,
+
        Parent = crate::object::Oid,
+
        Signatures = crate::ExtendedSignature,
+
    >,
{
    let references = storage
        .types(typename)
modified crates/radicle-cob/src/object/collaboration/remove.rs
@@ -1,6 +1,6 @@
// Copyright © 2022 The Radicle Link Contributors

-
use crate::{ObjectId, Store, TypeName};
+
use crate::{ObjectId, TypeName};

use super::error;

@@ -20,7 +20,7 @@ pub fn remove<S>(
    oid: &ObjectId,
) -> Result<(), error::Remove>
where
-
    S: Store,
+
    S: crate::object::Storage,
{
    storage
        .remove(identifier, typename, oid)
modified crates/radicle-cob/src/object/collaboration/update.rs
@@ -6,7 +6,7 @@ use oid::Oid;

use crate::{
    change, change_graph::ChangeGraph, history::EntryId, CollaborativeObject, Embed, Evaluate,
-
    ExtendedSignature, ObjectId, Store, TypeName,
+
    ExtendedSignature, ObjectId, TypeName,
};

use super::error;
@@ -65,7 +65,8 @@ pub fn update<T, S, G>(
) -> Result<Updated<T>, error::Update>
where
    T: Evaluate<S>,
-
    S: Store,
+
    S: crate::object::Storage,
+
    S: change::Storage<ObjectId = Oid, Parent = Oid, Signatures = ExtendedSignature>,
    G: signature::Signer<ExtendedSignature>,
{
    let Update {
@@ -86,18 +87,20 @@ where
        graph.evaluate(storage).map_err(error::Update::evaluate)?;

    // Create a commit for this change, but don't update any references yet.
-
    let entry = storage.store(
-
        resource,
-
        related,
-
        signer,
-
        change::Template {
-
            tips: object.history.tips().into_iter().collect(),
-
            embeds,
-
            contents: changes,
-
            type_name: typename.clone(),
-
            message,
-
        },
-
    )?;
+
    let entry = storage
+
        .store(
+
            resource,
+
            related,
+
            signer,
+
            change::Template {
+
                tips: object.history.tips().into_iter().collect(),
+
                embeds,
+
                contents: changes,
+
                type_name: typename.clone(),
+
                message,
+
            },
+
        )
+
        .map_err(Into::<error::Update>::into)?;
    let head = entry.id;
    let parents = entry.parents.to_vec();

modified crates/radicle-cob/src/object/storage.rs
@@ -96,13 +96,12 @@ pub mod convert {
    use git_ext::ref_format::RefString;
    use thiserror::Error;

-
    use super::{Commit, Reference};
-

    #[derive(Debug, Error)]
    pub enum Error {
        #[error("the reference '{name}' does not point to a commit object")]
        NotCommit {
            name: RefString,
+
            #[cfg(feature = "git2")]
            #[source]
            err: git2::Error,
        },
@@ -112,22 +111,25 @@ pub mod convert {
        Utf8(#[from] str::Utf8Error),
    }

-
    impl<'a> TryFrom<git2::Reference<'a>> for Reference {
+
    #[cfg(feature = "git2")]
+
    impl<'a> TryFrom<git2::Reference<'a>> for super::Reference {
        type Error = Error;

        fn try_from(value: git2::Reference<'a>) -> Result<Self, Self::Error> {
            let name = RefString::try_from(str::from_utf8(value.name_bytes())?)?;
-
            let target = Commit::from(value.peel_to_commit().map_err(|err| Error::NotCommit {
-
                name: name.clone(),
-
                err,
-
            })?);
+
            let target =
+
                super::Commit::from(value.peel_to_commit().map_err(|err| Error::NotCommit {
+
                    name: name.clone(),
+
                    err,
+
                })?);
            Ok(Self { name, target })
        }
    }

-
    impl<'a> From<git2::Commit<'a>> for Commit {
+
    #[cfg(feature = "git2")]
+
    impl<'a> From<git2::Commit<'a>> for super::Commit {
        fn from(commit: git2::Commit<'a>) -> Self {
-
            Commit {
+
            Self {
                id: commit.id().into(),
            }
        }
modified crates/radicle-cob/src/test.rs
@@ -1,7 +1,11 @@
+
#[cfg(feature = "git2")]
pub mod identity;
+
#[cfg(feature = "git2")]
pub use identity::{Person, Project, RemoteProject};

+
#[cfg(feature = "git2")]
pub mod storage;
+
#[cfg(feature = "git2")]
pub use storage::Storage;

pub mod arbitrary;
modified crates/radicle-cob/src/test/identity.rs
@@ -1,7 +1,9 @@
pub mod project;
pub use project::{Project, RemoteProject};

+
#[cfg(feature = "git2")]
pub mod person;
+
#[cfg(feature = "git2")]
pub use person::Person;

#[derive(Clone, Debug, PartialEq, Eq)]
modified crates/radicle-cob/src/tests.rs
@@ -1,238 +1,272 @@
-
use std::ops::ControlFlow;
-

-
use crypto::test::signer::MockSigner;
-
use crypto::{PublicKey, Signer};
use git_ext::ref_format::{refname, Component, RefString};
-
use nonempty::{nonempty, NonEmpty};
-
use qcheck::Arbitrary;
-

-
use crate::{
-
    create, get, list, object, test::arbitrary::Invalid, update, Create, Entry, ObjectId, TypeName,
-
    Update, Updated, Version,
-
};
-

-
use super::test;
-

-
#[test]
-
fn roundtrip() {
-
    let storage = test::Storage::new();
-
    let signer = gen::<MockSigner>(1);
-
    let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
-
    let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
-
    let proj = test::RemoteProject {
-
        project: proj,
-
        person: terry,
-
    };
-
    let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
-
    let cob = create::<NonEmpty<Entry>, _, _>(
-
        &storage,
-
        &signer,
-
        Some(proj.project.content_id),
-
        vec![],
-
        signer.public_key(),
-
        Create {
-
            contents: nonempty!(Vec::new()),
-
            type_name: typename.clone(),
-
            message: "creating xyz.rad.issue".to_string(),
-
            embeds: vec![],
-
            version: Version::default(),
-
        },
-
    )
-
    .unwrap();
-

-
    let expected = get(&storage, &typename, cob.id())
-
        .unwrap()
-
        .expect("BUG: cob was missing");
-

-
    assert_eq!(cob, expected);
-
}

-
#[test]
-
fn list_cobs() {
-
    let storage = test::Storage::new();
-
    let signer = gen::<MockSigner>(1);
-
    let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
-
    let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
-
    let proj = test::RemoteProject {
-
        project: proj,
-
        person: terry,
-
    };
-
    let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
-
    let issue_1 = create::<NonEmpty<Entry>, _, _>(
-
        &storage,
-
        &signer,
-
        Some(proj.project.content_id),
-
        vec![],
-
        signer.public_key(),
-
        Create {
-
            contents: nonempty!(b"issue 1".to_vec()),
-
            type_name: typename.clone(),
-
            message: "creating xyz.rad.issue".to_string(),
-
            embeds: vec![],
-
            version: Version::default(),
-
        },
-
    )
-
    .unwrap();
-

-
    let issue_2 = create(
-
        &storage,
-
        &signer,
-
        Some(proj.project.content_id),
-
        vec![],
-
        signer.public_key(),
-
        Create {
-
            contents: nonempty!(b"issue 2".to_vec()),
-
            type_name: typename.clone(),
-
            message: "commenting xyz.rad.issue".to_string(),
-
            embeds: vec![],
-
            version: Version::default(),
-
        },
-
    )
-
    .unwrap();
-

-
    let mut expected = list(&storage, &typename).unwrap();
-
    expected.sort_by(|x, y| x.id().cmp(y.id()));
-

-
    let mut actual = vec![issue_1, issue_2];
-
    actual.sort_by(|x, y| x.id().cmp(y.id()));
-

-
    assert_eq!(actual, expected);
-
}
+
use crate::{object, test::arbitrary::Invalid, ObjectId, TypeName};

-
#[test]
-
fn update_cob() {
-
    let storage = test::Storage::new();
-
    let signer = gen::<MockSigner>(1);
-
    let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
-
    let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
-
    let proj = test::RemoteProject {
-
        project: proj,
-
        person: terry,
-
    };
-
    let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
-
    let cob = create::<NonEmpty<Entry>, _, _>(
-
        &storage,
-
        &signer,
-
        Some(proj.project.content_id),
-
        vec![],
-
        signer.public_key(),
-
        Create {
-
            contents: nonempty!(Vec::new()),
-
            type_name: typename.clone(),
-
            message: "creating xyz.rad.issue".to_string(),
-
            embeds: vec![],
-
            version: Version::default(),
-
        },
-
    )
-
    .unwrap();
-

-
    let not_expected = get::<NonEmpty<Entry>, _>(&storage, &typename, cob.id())
-
        .unwrap()
-
        .expect("BUG: cob was missing");
-

-
    let Updated { object, .. } = update(
-
        &storage,
-
        &signer,
-
        Some(proj.project.content_id),
-
        vec![],
-
        signer.public_key(),
-
        Update {
-
            changes: nonempty!(b"issue 1".to_vec()),
-
            object_id: *cob.id(),
-
            type_name: typename.clone(),
-
            embeds: vec![],
-
            message: "commenting xyz.rad.issue".to_string(),
-
        },
-
    )
-
    .unwrap();
-

-
    let expected = get(&storage, &typename, object.id())
-
        .unwrap()
-
        .expect("BUG: cob was missing");
-

-
    assert_ne!(object, not_expected);
-
    assert_eq!(object, expected, "{object:#?} {expected:#?}");
-
}
-

-
#[test]
-
fn traverse_cobs() {
-
    let storage = test::Storage::new();
-
    let neil_signer = gen::<MockSigner>(2);
-
    let neil = test::Person::new(&storage, "gaiman", *neil_signer.public_key()).unwrap();
-
    let terry_signer = gen::<MockSigner>(1);
-
    let terry = test::Person::new(&storage, "pratchett", *terry_signer.public_key()).unwrap();
-
    let proj = test::Project::new(&storage, "discworld", *terry_signer.public_key()).unwrap();
-
    let terry_proj = test::RemoteProject {
-
        project: proj.clone(),
-
        person: terry,
-
    };
-
    let neil_proj = test::RemoteProject {
-
        project: proj,
-
        person: neil,
-
    };
-
    let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
-
    let cob = create::<NonEmpty<Entry>, _, _>(
-
        &storage,
-
        &terry_signer,
-
        Some(terry_proj.project.content_id),
-
        vec![],
-
        terry_signer.public_key(),
-
        Create {
-
            contents: nonempty!(b"issue 1".to_vec()),
-
            type_name: typename.clone(),
-
            message: "creating xyz.rad.issue".to_string(),
-
            embeds: vec![],
-
            version: Version::default(),
-
        },
-
    )
-
    .unwrap();
-
    copy_to(
-
        storage.as_raw(),
-
        terry_signer.public_key(),
-
        &neil_proj,
-
        &typename,
-
        *cob.id(),
-
    )
-
    .unwrap();
-

-
    let Updated { object, .. } = update::<NonEmpty<Entry>, _, _>(
-
        &storage,
-
        &neil_signer,
-
        Some(neil_proj.project.content_id),
-
        vec![],
-
        neil_signer.public_key(),
-
        Update {
-
            changes: nonempty!(b"issue 2".to_vec()),
-
            object_id: *cob.id(),
-
            type_name: typename,
-
            embeds: vec![],
-
            message: "commenting on xyz.rad.issue".to_string(),
-
        },
-
    )
-
    .unwrap();
-

-
    let root = object.history.root().id;
-
    // traverse over the history and filter by changes that were only authorized by terry
-
    let contents = object
-
        .history()
-
        .traverse(Vec::new(), &[root], |mut acc, _, entry| {
-
            if entry.author() == terry_signer.public_key() {
-
                acc.push(entry.contents().head.clone());
-
            }
-
            ControlFlow::Continue(acc)
-
        });
+
#[cfg(feature = "git2")]
+
mod git {
+
    use std::ops::ControlFlow;

-
    assert_eq!(contents, vec![b"issue 1".to_vec()]);
+
    use crypto::test::signer::MockSigner;
+
    use crypto::{PublicKey, Signer};
+
    use nonempty::{nonempty, NonEmpty};
+
    use qcheck::Arbitrary;

-
    // traverse over the history and filter by changes that were only authorized by neil
-
    let contents = object
-
        .history()
-
        .traverse(Vec::new(), &[root], |mut acc, _, entry| {
-
            acc.push(entry.contents().head.clone());
-
            ControlFlow::Continue(acc)
-
        });
+
    use crate::{
+
        create, get, list, update, Create, Entry, ObjectId, TypeName, Update, Updated, Version,
+
    };

-
    assert_eq!(contents, vec![b"issue 1".to_vec(), b"issue 2".to_vec()]);
+
    use crate::test;
+

+
    #[test]
+
    fn roundtrip() {
+
        let storage = test::Storage::new();
+
        let signer = gen::<MockSigner>(1);
+
        let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
+
        let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
+
        let proj = test::RemoteProject {
+
            project: proj,
+
            person: terry,
+
        };
+
        let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
+
        let cob = create::<NonEmpty<Entry>, _, _>(
+
            &storage,
+
            &signer,
+
            Some(proj.project.content_id),
+
            vec![],
+
            signer.public_key(),
+
            Create {
+
                contents: nonempty!(Vec::new()),
+
                type_name: typename.clone(),
+
                message: "creating xyz.rad.issue".to_string(),
+
                embeds: vec![],
+
                version: Version::default(),
+
            },
+
        )
+
        .unwrap();
+

+
        let expected = get(&storage, &typename, cob.id())
+
            .unwrap()
+
            .expect("BUG: cob was missing");
+

+
        assert_eq!(cob, expected);
+
    }
+

+
    #[test]
+
    fn list_cobs() {
+
        let storage = test::Storage::new();
+
        let signer = gen::<MockSigner>(1);
+
        let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
+
        let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
+
        let proj = test::RemoteProject {
+
            project: proj,
+
            person: terry,
+
        };
+
        let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
+
        let issue_1 = create::<NonEmpty<Entry>, _, _>(
+
            &storage,
+
            &signer,
+
            Some(proj.project.content_id),
+
            vec![],
+
            signer.public_key(),
+
            Create {
+
                contents: nonempty!(b"issue 1".to_vec()),
+
                type_name: typename.clone(),
+
                message: "creating xyz.rad.issue".to_string(),
+
                embeds: vec![],
+
                version: Version::default(),
+
            },
+
        )
+
        .unwrap();
+

+
        let issue_2 = create(
+
            &storage,
+
            &signer,
+
            Some(proj.project.content_id),
+
            vec![],
+
            signer.public_key(),
+
            Create {
+
                contents: nonempty!(b"issue 2".to_vec()),
+
                type_name: typename.clone(),
+
                message: "commenting xyz.rad.issue".to_string(),
+
                embeds: vec![],
+
                version: Version::default(),
+
            },
+
        )
+
        .unwrap();
+

+
        let mut expected = list(&storage, &typename).unwrap();
+
        expected.sort_by(|x, y| x.id().cmp(y.id()));
+

+
        let mut actual = vec![issue_1, issue_2];
+
        actual.sort_by(|x, y| x.id().cmp(y.id()));
+

+
        assert_eq!(actual, expected);
+
    }
+

+
    #[test]
+
    fn update_cob() {
+
        let storage = test::Storage::new();
+
        let signer = gen::<MockSigner>(1);
+
        let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
+
        let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
+
        let proj = test::RemoteProject {
+
            project: proj,
+
            person: terry,
+
        };
+
        let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
+
        let cob = create::<NonEmpty<Entry>, _, _>(
+
            &storage,
+
            &signer,
+
            Some(proj.project.content_id),
+
            vec![],
+
            signer.public_key(),
+
            Create {
+
                contents: nonempty!(Vec::new()),
+
                type_name: typename.clone(),
+
                message: "creating xyz.rad.issue".to_string(),
+
                embeds: vec![],
+
                version: Version::default(),
+
            },
+
        )
+
        .unwrap();
+

+
        let not_expected = get::<NonEmpty<Entry>, _>(&storage, &typename, cob.id())
+
            .unwrap()
+
            .expect("BUG: cob was missing");
+

+
        let Updated { object, .. } = update(
+
            &storage,
+
            &signer,
+
            Some(proj.project.content_id),
+
            vec![],
+
            signer.public_key(),
+
            Update {
+
                changes: nonempty!(b"issue 1".to_vec()),
+
                object_id: *cob.id(),
+
                type_name: typename.clone(),
+
                embeds: vec![],
+
                message: "commenting xyz.rad.issue".to_string(),
+
            },
+
        )
+
        .unwrap();
+

+
        let expected = get(&storage, &typename, object.id())
+
            .unwrap()
+
            .expect("BUG: cob was missing");
+

+
        assert_ne!(object, not_expected);
+
        assert_eq!(object, expected, "{object:#?} {expected:#?}");
+
    }
+

+
    #[test]
+
    fn traverse_cobs() {
+
        let storage = test::Storage::new();
+
        let neil_signer = gen::<MockSigner>(2);
+
        let neil = test::Person::new(&storage, "gaiman", *neil_signer.public_key()).unwrap();
+
        let terry_signer = gen::<MockSigner>(1);
+
        let terry = test::Person::new(&storage, "pratchett", *terry_signer.public_key()).unwrap();
+
        let proj = test::Project::new(&storage, "discworld", *terry_signer.public_key()).unwrap();
+
        let terry_proj = test::RemoteProject {
+
            project: proj.clone(),
+
            person: terry,
+
        };
+
        let neil_proj = test::RemoteProject {
+
            project: proj,
+
            person: neil,
+
        };
+
        let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
+
        let cob = create::<NonEmpty<Entry>, _, _>(
+
            &storage,
+
            &terry_signer,
+
            Some(terry_proj.project.content_id),
+
            vec![],
+
            terry_signer.public_key(),
+
            Create {
+
                contents: nonempty!(b"issue 1".to_vec()),
+
                type_name: typename.clone(),
+
                message: "creating xyz.rad.issue".to_string(),
+
                embeds: vec![],
+
                version: Version::default(),
+
            },
+
        )
+
        .unwrap();
+
        copy_to(
+
            storage.as_raw(),
+
            terry_signer.public_key(),
+
            &neil_proj,
+
            &typename,
+
            *cob.id(),
+
        )
+
        .unwrap();
+

+
        let Updated { object, .. } = update::<NonEmpty<Entry>, _, _>(
+
            &storage,
+
            &neil_signer,
+
            Some(neil_proj.project.content_id),
+
            vec![],
+
            neil_signer.public_key(),
+
            Update {
+
                changes: nonempty!(b"issue 2".to_vec()),
+
                object_id: *cob.id(),
+
                type_name: typename,
+
                embeds: vec![],
+
                message: "commenting on xyz.rad.issue".to_string(),
+
            },
+
        )
+
        .unwrap();
+

+
        let root = object.history.root().id;
+
        // traverse over the history and filter by changes that were only authorized by terry
+
        let contents = object
+
            .history()
+
            .traverse(Vec::new(), &[root], |mut acc, _, entry| {
+
                if entry.author() == terry_signer.public_key() {
+
                    acc.push(entry.contents().head.clone());
+
                }
+
                ControlFlow::Continue(acc)
+
            });
+

+
        assert_eq!(contents, vec![b"issue 1".to_vec()]);
+

+
        // traverse over the history and filter by changes that were only authorized by neil
+
        let contents = object
+
            .history()
+
            .traverse(Vec::new(), &[root], |mut acc, _, entry| {
+
                acc.push(entry.contents().head.clone());
+
                ControlFlow::Continue(acc)
+
            });
+

+
        assert_eq!(contents, vec![b"issue 1".to_vec(), b"issue 2".to_vec()]);
+
    }
+

+
    fn copy_to(
+
        repo: &git2::Repository,
+
        from: &PublicKey,
+
        to: &test::RemoteProject,
+
        typename: &TypeName,
+
        object: ObjectId,
+
    ) -> Result<(), git2::Error> {
+
        let original = {
+
            let name = format!("refs/rad/{from}/cobs/{typename}/{object}");
+
            let r = repo.find_reference(&name)?;
+
            r.target().unwrap()
+
        };
+

+
        let name = format!(
+
            "refs/rad/{}/cobs/{}/{}",
+
            to.identifier().to_path(),
+
            typename,
+
            object
+
        );
+
        repo.reference(&name, original, false, "copying object reference")?;
+
        Ok(())
+
    }
+

+
    fn gen<T: Arbitrary>(size: usize) -> T {
+
        let mut gen = qcheck::Gen::new(size);
+

+
        T::arbitrary(&mut gen)
+
    }
}

#[quickcheck]
@@ -297,32 +331,3 @@ fn invalid_parse_refstr(oid: Invalid<ObjectId>, typename: TypeName) {
        None
    );
}
-

-
fn gen<T: Arbitrary>(size: usize) -> T {
-
    let mut gen = qcheck::Gen::new(size);
-

-
    T::arbitrary(&mut gen)
-
}
-

-
fn copy_to(
-
    repo: &git2::Repository,
-
    from: &PublicKey,
-
    to: &test::RemoteProject,
-
    typename: &TypeName,
-
    object: ObjectId,
-
) -> Result<(), git2::Error> {
-
    let original = {
-
        let name = format!("refs/rad/{from}/cobs/{typename}/{object}");
-
        let r = repo.find_reference(&name)?;
-
        r.target().unwrap()
-
    };
-

-
    let name = format!(
-
        "refs/rad/{}/cobs/{}/{}",
-
        to.identifier().to_path(),
-
        typename,
-
        object
-
    );
-
    repo.reference(&name, original, false, "copying object reference")?;
-
    Ok(())
-
}
modified crates/radicle/Cargo.toml
@@ -11,7 +11,7 @@ rust-version.workspace = true

[features]
default = []
-
test = ["qcheck", "radicle-crypto/test"]
+
test = ["qcheck", "radicle-crypto/test", "radicle-cob/test"]
logger = ["colored", "chrono"]

[dependencies]
@@ -32,7 +32,7 @@ log = { workspace = true, features = ["std"] }
multibase = { workspace = true }
nonempty = { workspace = true, features = ["serialize"] }
qcheck = { workspace = true, optional = true }
-
radicle-cob = { workspace = true }
+
radicle-cob = { workspace = true, features = ["git2"] }
radicle-crypto = { workspace = true, features = ["radicle-git-ext", "ssh", "sqlite", "cyphernet"] }
radicle-git-ext = { workspace = true, features = ["serde"] }
radicle-ssh = { workspace = true }
@@ -58,5 +58,5 @@ jsonschema = { version = "0.30", default-features = false }
pretty_assertions = { workspace = true }
qcheck = { workspace = true }
qcheck-macros = { workspace = true }
-
radicle-cob = { workspace = true, features = ["stable-commit-ids"] }
+
radicle-cob = { workspace = true, features = ["stable-commit-ids", "test"] }
radicle-crypto = { workspace = true, features = ["test"] }