Radish alpha
r
rad:zwTxygwuz5LDGBq255RA2CbNGrz8
Radicle CI broker
Radicle
Git
feat: add module radicle_ci_broker::ergo
Merged liw opened 8 months ago

Signed-off-by: Lars Wirzenius liw@liw.fi

refactor: use ergo module in trigger command

Signed-off-by: Lars Wirzenius liw@liw.fi

refactor: use ergo in event commands

Signed-off-by: Lars Wirzenius liw@liw.fi

refactor: use ergo module for run commands

Signed-off-by: Lars Wirzenius liw@liw.fi

6 files changed +295 -188 80db71a4 7c3af34a
modified src/bin/cibtool.rs
@@ -18,12 +18,7 @@ use std::{

use clap::Parser;

-
use radicle::{
-
    git::Oid,
-
    prelude::{NodeId, RepoId},
-
    storage::ReadStorage,
-
    Profile, Storage,
-
};
+
use radicle::{git::Oid, prelude::NodeId, Profile};

use radicle_ci_broker::{
    broker::BrokerError,
@@ -253,33 +248,9 @@ enum CibToolError {
    #[error("failed to look up node profile")]
    Profile(#[source] radicle::profile::Error),

-
    #[error("failed to look up open node storage")]
-
    Storage(#[source] radicle::storage::Error),
-

-
    #[error("failed to list repositories in node storage")]
-
    Repositories(#[source] radicle::storage::Error),
-

-
    #[error("failed to look up project info for repository {0}")]
-
    Project(RepoId, #[source] radicle::identity::doc::PayloadError),
-

-
    #[error("node has more than one repository called {0}")]
-
    DuplicateRepositories(String),
-

-
    #[error("node has no repository called: {0}")]
-
    NotFound(String),
-

    #[error("cannot find CI run with id {0}")]
    RunNotFound(RunId),

-
    #[error("failed to open git repository in node storage: {0}")]
-
    RepoOpen(RepoId, #[source] radicle::storage::RepositoryError),
-

-
    #[error("failed to get project information for {0}")]
-
    GetProject(RepoId, #[source] radicle::storage::RepositoryError),
-

-
    #[error("failed to parse git ref as a commit id: {0}")]
-
    RevParse(String, #[source] radicle::git::raw::Error),
-

    #[error(transparent)]
    Broker(#[from] BrokerError),

@@ -389,4 +360,10 @@ enum CibToolError {

    #[error(transparent)]
    Trigger(#[from] cibtoolcmd::TriggerError),
+

+
    #[error(transparent)]
+
    Event(#[from] cibtoolcmd::EventError),
+

+
    #[error(transparent)]
+
    Run(#[from] cibtoolcmd::RunError),
}
modified src/bin/cibtoolcmd/event.rs
@@ -2,11 +2,10 @@ use std::io::Write;

use clap::ValueEnum;

-
use radicle::patch::PatchId;
-
use radicle_ci_broker::refs::branch_ref;
+
use radicle::{git::BranchName, patch::PatchId, storage::git::Repository};
use radicle_ci_broker::{
-
    filter::EventFilter, node_event_source::NodeEventSource, refs::ref_string,
-
    util::read_file_as_objectid,
+
    ergo, filter::EventFilter, node_event_source::NodeEventSource, refs::branch_ref,
+
    refs::ref_string, util::read_file_as_objectid,
};

use super::*;
@@ -117,92 +116,39 @@ pub struct AddEvent {
}

impl AddEvent {
-
    fn lookup_nid(&self) -> Result<NodeId, CibToolError> {
-
        let profile = Profile::load().map_err(CibToolError::Profile)?;
-
        Ok(*profile.id())
-
    }
-

-
    fn lookup_rid(&self, wanted: &str) -> Result<RepoId, CibToolError> {
-
        let profile = Profile::load().map_err(CibToolError::Profile)?;
-
        let storage =
-
            Storage::open(profile.storage(), profile.info()).map_err(CibToolError::Storage)?;
-

-
        let mut rid = None;
-
        let repo_infos = storage.repositories().map_err(CibToolError::Repositories)?;
-
        for ri in repo_infos {
-
            let project = ri
-
                .doc
-
                .project()
-
                .map_err(|e| CibToolError::Project(ri.rid, e))?;
-

-
            if project.name() == wanted {
-
                if rid.is_some() {
-
                    return Err(CibToolError::DuplicateRepositories(wanted.into()));
-
                }
-
                rid = Some(ri.rid);
-
            }
-
        }
-

-
        if let Some(rid) = rid {
-
            Ok(rid)
+
    fn branch(&self, r: &ergo::Radicle, repo: &Repository) -> Result<BranchName, TriggerError> {
+
        if let Some(name) = &self.name {
+
            Ok(branch_ref(&ref_string(name)?)?)
        } else {
-
            Err(CibToolError::NotFound(wanted.into()))
+
            let project = r.project(&repo.id).map_err(TriggerError::Ergonomic)?;
+
            Ok(project.default_branch().clone())
        }
    }
-

-
    fn lookup_commit(&self, rid: RepoId, gitref: &str) -> Result<Oid, CibToolError> {
-
        let profile = Profile::load().map_err(CibToolError::Profile)?;
-
        let storage =
-
            Storage::open(profile.storage(), profile.info()).map_err(CibToolError::Storage)?;
-
        let repo = storage
-
            .repository(rid)
-
            .map_err(|e| CibToolError::RepoOpen(rid, e))?;
-
        let object = repo
-
            .backend
-
            .revparse_single(gitref)
-
            .map_err(|e| CibToolError::RevParse(gitref.into(), e))?;
-

-
        Ok(object.id().into())
-
    }
}

impl Leaf for AddEvent {
    fn run(&self, args: &Args) -> Result<(), CibToolError> {
-
        let nid = self.lookup_nid()?;
+
        let r = ergo::Radicle::new().map_err(EventError::Ergonomic)?;

-
        let rid = if let Ok(rid) = RepoId::from_urn(&self.repo) {
-
            rid
-
        } else {
-
            self.lookup_rid(&self.repo)?
-
        };
+
        let profile = r.profile();
+
        let nid = *profile.id();

-
        let oid = if let Ok(rid) = Oid::from_str(&self.commit) {
-
            rid
-
        } else {
-
            self.lookup_commit(rid, &self.commit)?
-
        };
+
        let repo = r
+
            .repository_by_name(&self.repo)
+
            .map_err(EventError::Ergonomic)?;

-
        let branch_name = if let Some(name) = &self.name {
-
            branch_ref(&ref_string(name).map_err(CibToolError::RefError)?)
-
                .map_err(CibToolError::RefError)?
-
        } else {
-
            let profile = util::load_profile()?;
-
            let repo = profile
-
                .storage
-
                .repository(rid)
-
                .map_err(|err| CibToolError::RepoOpen(rid, err))?;
-
            let project = repo
-
                .project()
-
                .map_err(|err| CibToolError::GetProject(rid, err))?;
-
            project.default_branch().clone()
-
        };
+
        let oid = r
+
            .resolve_commit(&repo.id, &self.commit)
+
            .map_err(EventError::Ergonomic)?;
+

+
        let branch_name = self.branch(&r, &repo)?;

        let event = match &self.kind {
            EventKind::BranchCreated => {
                if self.base.is_some() {
                    return Err(CibToolError::NoBaseAllowed);
                } else {
-
                    CiEvent::branch_created(nid, rid, &branch_name, oid)
+
                    CiEvent::branch_created(nid, repo.id, &branch_name, oid)
                        .map_err(CibToolError::CiEvent)?
                }
            }
@@ -211,32 +157,33 @@ impl Leaf for AddEvent {
                    let base = if let Ok(base) = Oid::from_str(base) {
                        base
                    } else {
-
                        self.lookup_commit(rid, base)?
+
                        r.resolve_commit(&repo.id, base)
+
                            .map_err(EventError::Ergonomic)?
                    };
-
                    CiEvent::branch_updated(nid, rid, &branch_name, oid, base)
+
                    CiEvent::branch_updated(nid, repo.id, &branch_name, oid, base)
                        .map_err(CibToolError::CiEvent)?
                } else {
                    return Err(CibToolError::BaseRequired);
                }
            }
-
            EventKind::BranchDeleted => CiEvent::branch_deleted(nid, rid, &branch_name, oid)
+
            EventKind::BranchDeleted => CiEvent::branch_deleted(nid, repo.id, &branch_name, oid)
                .map_err(CibToolError::CiEvent)?,
            EventKind::PatchCreated => {
                if let Some(patch_id) = &self.patch_id {
-
                    CiEvent::patch_created(nid, rid, *patch_id, oid)
+
                    CiEvent::patch_created(nid, repo.id, *patch_id, oid)
                } else if let Some(filename) = &self.patch_id_file {
                    let patch_id = read_file_as_objectid(filename)?;
-
                    CiEvent::patch_created(nid, rid, patch_id, oid)
+
                    CiEvent::patch_created(nid, repo.id, patch_id, oid)
                } else {
                    return Err(CibToolError::PatchIdRequired);
                }
            }
            EventKind::PatchUpdated => {
                if let Some(patch_id) = &self.patch_id {
-
                    CiEvent::patch_updated(nid, rid, *patch_id, oid)
+
                    CiEvent::patch_updated(nid, repo.id, *patch_id, oid)
                } else if let Some(filename) = &self.patch_id_file {
                    let patch_id = read_file_as_objectid(filename)?;
-
                    CiEvent::patch_created(nid, rid, patch_id, oid)
+
                    CiEvent::patch_created(nid, repo.id, patch_id, oid)
                } else {
                    return Err(CibToolError::PatchIdRequired);
                }
@@ -555,3 +502,12 @@ impl Leaf for FilterEvents {
        Ok(())
    }
}
+

+
#[derive(Debug, thiserror::Error)]
+
pub enum EventError {
+
    #[error(transparent)]
+
    Ergonomic(#[from] radicle_ci_broker::ergo::ErgoError),
+

+
    #[error(transparent)]
+
    RefError(#[from] radicle_ci_broker::refs::RefError),
+
}
modified src/bin/cibtoolcmd/run.rs
@@ -1,6 +1,6 @@
use std::collections::HashSet;

-
use radicle_ci_broker::run::RunBuilder;
+
use radicle_ci_broker::{ergo, run::RunBuilder};
use radicle_job::JobId;

use super::*;
@@ -62,9 +62,18 @@ impl Leaf for AddRun {
    fn run(&self, args: &Args) -> Result<(), CibToolError> {
        let db = args.open_db()?;

-
        let profile = util::load_profile()?;
-
        let (rid, repo_name) = util::lookup_repo(&profile, &self.repo)?;
-
        let oid = util::oid_from_cli_arg(&profile, rid, &self.commit)?;
+
        let r = ergo::Radicle::new().map_err(RunError::Ergonomic)?;
+
        let repo = r
+
            .repository_by_name(&self.repo)
+
            .map_err(RunError::Ergonomic)?;
+
        let repo_name = r
+
            .project(&repo.id)
+
            .map_err(RunError::Ergonomic)?
+
            .name()
+
            .to_string();
+
        let oid = r
+
            .resolve_commit(&repo.id, &self.commit)
+
            .map_err(RunError::Ergonomic)?;
        let ts = self.timestamp.clone().unwrap_or(util::now()?);

        let whence = Whence::Branch {
@@ -74,7 +83,7 @@ impl Leaf for AddRun {
        };
        let mut run = RunBuilder::default()
            .broker_run_id(RunId::default())
-
            .repo_id(rid)
+
            .repo_id(repo.id)
            .repo_name(&repo_name)
            .whence(whence)
            .timestamp(ts)
@@ -264,3 +273,9 @@ impl Leaf for ListRuns {
        Ok(())
    }
}
+

+
#[derive(Debug, thiserror::Error)]
+
pub enum RunError {
+
    #[error(transparent)]
+
    Ergonomic(#[from] radicle_ci_broker::ergo::ErgoError),
+
}
modified src/bin/cibtoolcmd/trigger.rs
@@ -1,11 +1,9 @@
-
use radicle::{
-
    cob::patch::{cache::Patches, Patch, PatchId},
-
    identity::RepoId,
-
    profile::Profile,
-
    storage::ReadStorage,
-
};
+
use radicle::{cob::patch::PatchId, git::BranchName, storage::git::Repository};

-
use radicle_ci_broker::refs::{branch_ref, ref_string};
+
use radicle_ci_broker::{
+
    ergo,
+
    refs::{branch_ref, ref_string},
+
};

use super::*;

@@ -53,41 +51,23 @@ pub struct TriggerCmd {

impl Leaf for TriggerCmd {
    fn run(&self, args: &Args) -> Result<(), CibToolError> {
-
        let profile = util::load_profile()?;
-
        let nid = util::lookup_nid(&profile)?;
-
        let nid = self.node.unwrap_or(nid);
-
        let (rid, _repo_name) = util::lookup_repo(&profile, &self.repo)?;
+
        let r = ergo::Radicle::new().map_err(TriggerError::Ergonomic)?;

-
        let oid = if let Some(commit) = &self.commit {
-
            util::oid_from_cli_arg(&profile, rid, commit)?
-
        } else if let Some(wanted) = &self.patch {
-
            let mut oid = None;
-
            for (patch_id, patch) in patches(&profile, &rid)? {
-
                if &patch_id == wanted {
-
                    oid = Some(*patch.head());
-
                    break;
-
                }
-
            }
-
            oid.ok_or(TriggerError::NoSuchPatch(*wanted, rid))?
-
        } else {
-
            util::oid_from_cli_arg(&profile, rid, "HEAD")?
-
        };
+
        let profile = r.profile();
+
        let nid = self.node.unwrap_or(*profile.id());
+
        let repo = r
+
            .repository_by_name(&self.repo)
+
            .map_err(TriggerError::Ergonomic)?;

-
        let base = util::lookup_commit(&profile, rid, &format!("{oid}^")).unwrap_or(oid);
-
        let branch_name = if let Some(name) = &self.name {
-
            branch_ref(&ref_string(name).map_err(CibToolError::RefError)?)
-
                .map_err(CibToolError::RefError)?
-
        } else {
-
            let repo = profile
-
                .storage
-
                .repository(rid)
-
                .map_err(|err| CibToolError::RepoOpen(rid, err))?;
-
            let project = repo
-
                .project()
-
                .map_err(|err| CibToolError::GetProject(rid, err))?;
-
            project.default_branch().clone()
-
        };
-
        let event = CiEvent::branch_updated(nid, rid, &branch_name, oid, base)
+
        let oid = self.oid(&r, &repo)?;
+

+
        let base = r
+
            .resolve_commit(&repo.id, &format!("{oid}^"))
+
            .unwrap_or(oid);
+

+
        let branch_name = self.branch(&r, &repo)?;
+

+
        let event = CiEvent::branch_updated(nid, repo.id, &branch_name, oid, base)
            .map_err(CibToolError::CiEvent)?;

        if self.stdout {
@@ -112,42 +92,33 @@ impl Leaf for TriggerCmd {
    }
}

-
pub fn patches(profile: &Profile, repo_id: &RepoId) -> Result<Vec<(PatchId, Patch)>, TriggerError> {
-
    let repo = profile
-
        .storage
-
        .repository(*repo_id)
-
        .map_err(|err| TriggerError::LoadRepo(*repo_id, Box::new(err)))?;
-

-
    let patches = profile
-
        .home
-
        .patches(&repo)
-
        .map_err(|err| TriggerError::LoadPathces(*repo_id, Box::new(err)))?;
-

-
    let mut items = vec![];
-
    let list = patches
-
        .list()
-
        .map_err(|err| TriggerError::ListCache(*repo_id, err))?;
-
    for result in list {
-
        let (id, patch) = result.map_err(|err| TriggerError::CacheListItem(*repo_id, err))?;
-
        items.push((id, patch));
+
impl TriggerCmd {
+
    fn oid(&self, r: &ergo::Radicle, repo: &Repository) -> Result<Oid, TriggerError> {
+
        let oid = if let Some(commit) = &self.commit {
+
            r.resolve_commit(&repo.id, commit)?
+
        } else if let Some(wanted) = &self.patch {
+
            *r.patch(&repo.id, wanted)?.head()
+
        } else {
+
            r.resolve_commit(&repo.id, "HEAD")?
+
        };
+
        Ok(oid)
+
    }
+

+
    fn branch(&self, r: &ergo::Radicle, repo: &Repository) -> Result<BranchName, TriggerError> {
+
        if let Some(name) = &self.name {
+
            Ok(branch_ref(&ref_string(name)?)?)
+
        } else {
+
            let project = r.project(&repo.id).map_err(TriggerError::Ergonomic)?;
+
            Ok(project.default_branch().clone())
+
        }
    }
-
    Ok(items)
}

#[derive(Debug, thiserror::Error)]
pub enum TriggerError {
-
    #[error("failed to load info from Radicle node storage for repository {0}")]
-
    LoadRepo(RepoId, #[source] Box<radicle::storage::RepositoryError>),
-

-
    #[error("failed to load patch list from Radicle node storage for repository {0}")]
-
    LoadPathces(RepoId, #[source] Box<radicle::profile::Error>),
-

-
    #[error("failed to list patches for repository {0}")]
-
    ListCache(RepoId, #[source] radicle::patch::cache::Error),
-

-
    #[error("failed to list info for patch {0}")]
-
    CacheListItem(RepoId, #[source] radicle::patch::cache::Error),
+
    #[error(transparent)]
+
    Ergonomic(#[from] radicle_ci_broker::ergo::ErgoError),

-
    #[error("can't find patch {0} in repository {1}")]
-
    NoSuchPatch(PatchId, RepoId),
+
    #[error(transparent)]
+
    RefError(#[from] radicle_ci_broker::refs::RefError),
}
added src/ergo.rs
@@ -0,0 +1,187 @@
+
//! An ergonomic wrapper around the `radicle` crate.
+
//!
+
//! The purpose of this module is to make it more convenient to use
+
//! the `radicle` crate to access a [Radicle](https://radicle.xyz/)
+
//! node and information in the node. It is not, in any way, meant to
+
//! be a replacement for using the official crate directly.
+

+
use std::str::FromStr;
+

+
use radicle::{
+
    cob::patch::{cache::Patches, Patch, PatchId},
+
    git::Oid,
+
    identity::{Project, RepoId},
+
    profile::Profile,
+
    storage::{git::Repository, ReadStorage, RepositoryInfo},
+
};
+

+
/// A Radicle node.
+
///
+
/// This type represents a Radicle node, and exists to cache some
+
/// stuff so it doesn't need to be re-loaded on every function call.
+
/// Especially the node profile.
+
pub struct Radicle {
+
    profile: Profile,
+
}
+

+
impl Radicle {
+
    /// Create a new [`Radicle`]. This may fail.
+
    pub fn new() -> Result<Self, ErgoError> {
+
        Ok(Self {
+
            profile: Profile::load().map_err(ErgoError::LoadProfile)?,
+
        })
+
    }
+

+
    /// Return the loaded profile.
+
    pub fn profile(&self) -> &Profile {
+
        &self.profile
+
    }
+

+
    /// List all repositories on a node.
+
    pub fn repositories(&self) -> Result<Vec<RepositoryInfo>, ErgoError> {
+
        self.profile
+
            .storage
+
            .repositories()
+
            .map_err(ErgoError::ListRepositories)
+
    }
+

+
    /// Load information about a specific repository.
+
    pub fn repository(&self, repo_id: &RepoId) -> Result<Repository, ErgoError> {
+
        self.profile
+
            .storage
+
            .repository(*repo_id)
+
            .map_err(|err| ErgoError::LoadRepo(*repo_id, Box::new(err)))
+
    }
+

+
    /// Load a repository by name, if the name is unique.
+
    pub fn repository_by_name(&self, wanted: &str) -> Result<Repository, ErgoError> {
+
        let matching: Result<Vec<RepoId>, ErgoError> = self
+
            .repositories()?
+
            .iter()
+
            .filter_map(|ri| match self.project(&ri.rid) {
+
                Ok(project) if project.name() == wanted => Some(Ok(ri.rid)),
+
                Err(err) => Some(Err(err)),
+
                _ => None,
+
            })
+
            .collect();
+
        let matching = matching?;
+

+
        match matching[..] {
+
            [] => Err(ErgoError::NoRepositoryWithName(wanted.to_string())),
+
            [_] => {
+
                let repo_id = matching[0];
+
                let repo = self.repository(&repo_id)?;
+
                Ok(repo)
+
            }
+
            [_, _, ..] => Err(ErgoError::NameIsNotUnique(wanted.to_string())),
+
        }
+
    }
+

+
    /// Load the project payload in the identity document of a
+
    /// repository.
+
    pub fn project(&self, repo_id: &RepoId) -> Result<Project, ErgoError> {
+
        let repo = self.repository(repo_id)?;
+
        repo.project()
+
            .map_err(|err| ErgoError::LoadProject(*repo_id, Box::new(err)))
+
    }
+

+
    /// Load all patches in a repository.
+
    pub fn patches(&self, repo_id: &RepoId) -> Result<Vec<(PatchId, Patch)>, ErgoError> {
+
        let repo = self.repository(repo_id)?;
+
        let patches = self
+
            .profile
+
            .home
+
            .patches(&repo)
+
            .map_err(|err| ErgoError::LoadPathces(*repo_id, Box::new(err)))?;
+
        let mut items = vec![];
+
        let list = patches
+
            .list()
+
            .map_err(|err| ErgoError::ListCache(*repo_id, err))?;
+
        for result in list {
+
            let (id, patch) = result.map_err(|err| ErgoError::CacheListItem(*repo_id, err))?;
+
            items.push((id, patch));
+
        }
+
        Ok(items)
+
    }
+

+
    /// Load a specific patch.
+
    pub fn patch(&self, repo_id: &RepoId, patch_id: &PatchId) -> Result<Patch, ErgoError> {
+
        let repo = self.repository(repo_id)?;
+
        let patches = self
+
            .profile
+
            .home
+
            .patches(&repo)
+
            .map_err(|err| ErgoError::LoadPathces(*repo_id, Box::new(err)))?;
+
        patches
+
            .get(patch_id)
+
            .map_err(|err| ErgoError::GetPatch(*repo_id, *patch_id, Box::new(err)))?
+
            .ok_or(ErgoError::NoSuchPatch(*repo_id, *patch_id))
+
    }
+

+
    /// Resolve a shortened patch ID into a full patch ID.
+
    pub fn resolve_patch_id(&self, repo_id: &RepoId, id: &str) -> Result<PatchId, ErgoError> {
+
        let repo = self.repository(repo_id)?;
+
        let object = repo
+
            .backend
+
            .revparse_single(id)
+
            .map_err(|err| ErgoError::ResolvePatchId(id.to_string(), err))?;
+
        Ok(PatchId::from(object.id()))
+
    }
+

+
    /// Resolve a short commit or name into a full commit ID.
+
    pub fn resolve_commit(&self, repo_id: &RepoId, gitref: &str) -> Result<Oid, ErgoError> {
+
        if let Ok(oid) = Oid::from_str(gitref) {
+
            Ok(oid)
+
        } else {
+
            let repo = self.repository(repo_id)?;
+
            let object = repo
+
                .backend
+
                .revparse_single(gitref)
+
                .map_err(|err| ErgoError::ResolveCommit(gitref.to_string(), *repo_id, err))?;
+
            Ok(Oid::from(object.id()))
+
        }
+
    }
+
}
+

+
/// Errors from the `Radicle` type.
+
#[derive(Debug, thiserror::Error)]
+
pub enum ErgoError {
+
    #[error("failed to load Radicle profile")]
+
    LoadProfile(#[source] radicle::profile::Error),
+

+
    #[error("failed to list repositories in Radicle node storage")]
+
    ListRepositories(#[source] radicle::storage::Error),
+

+
    #[error("failed to load info from Radicle node storage for repository {0}")]
+
    LoadRepo(RepoId, #[source] Box<radicle::storage::RepositoryError>),
+

+
    #[error("failed to load project info from Radicle node storage for repository {0}")]
+
    LoadProject(RepoId, #[source] Box<radicle::storage::RepositoryError>),
+

+
    #[error("failed to load patch list from Radicle node storage for repository {0}")]
+
    LoadPathces(RepoId, #[source] Box<radicle::profile::Error>),
+

+
    #[error("failed to list patches for repository {0}")]
+
    ListCache(RepoId, #[source] radicle::patch::cache::Error),
+

+
    #[error("failed to list info for patch {0}")]
+
    CacheListItem(RepoId, #[source] radicle::patch::cache::Error),
+

+
    #[error("failed to resolve patch id {0:?} in repository {1}")]
+
    ResolvePatchId(String, #[source] radicle::storage::git::raw::Error),
+

+
    #[error("failed to resolve commit id {0:?} in repository {1}")]
+
    ResolveCommit(String, RepoId, #[source] radicle::storage::git::raw::Error),
+

+
    #[error("failed to load patch {1} from Radicle node storage for repository {0}")]
+
    GetPatch(RepoId, PatchId, #[source] Box<radicle::patch::cache::Error>),
+

+
    #[error("no patch {1} in repository {0}")]
+
    NoSuchPatch(RepoId, PatchId),
+

+
    #[error("no repository called {0:?}")]
+
    NoRepositoryWithName(String),
+

+
    #[error("repository name is not unique: {0:?}")]
+
    NameIsNotUnique(String),
+
}
modified src/lib.rs
@@ -15,6 +15,7 @@ pub mod ci_event_source;
pub mod cob;
pub mod config;
pub mod db;
+
pub mod ergo;
pub mod filter;
pub mod logger;
pub mod msg;