Radish alpha
r
Radicle desktop app
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add Releases / ReleasesMut port traits
Daniel Norman committed 7 days ago
commit 7cc9bd20f09995bf13e6790f1c0c67f78d85c3a2
parent 9dad32757622cc532e1ee5f21be289687272fee2
4 files changed +256 -0
modified crates/radicle-types/src/lib.rs
@@ -2,6 +2,8 @@ use traits::cobs::Cobs;
use traits::issue::{Issues, IssuesMut};
use traits::job::Jobs;
use traits::patch::{Patches, PatchesMut};
+
use traits::release::Releases;
+
use traits::release_mut::ReleasesMut;
use traits::repo::Repo;
use traits::thread::Thread;
use traits::Profile;
@@ -43,6 +45,8 @@ impl IssuesMut for AppState {}
impl Jobs for AppState {}
impl Patches for AppState {}
impl PatchesMut for AppState {}
+
impl Releases for AppState {}
+
impl ReleasesMut for AppState {}
impl Profile for AppState {
    fn profile(&self) -> radicle::Profile {
        self.profile.clone()
modified crates/radicle-types/src/traits.rs
@@ -6,6 +6,8 @@ pub mod cobs;
pub mod issue;
pub mod job;
pub mod patch;
+
pub mod release;
+
pub mod release_mut;
pub mod repo;
pub mod thread;

added crates/radicle-types/src/traits/release.rs
@@ -0,0 +1,91 @@
+
use std::path::PathBuf;
+
use std::str::FromStr;
+

+
use radicle::identity;
+
use radicle::storage::ReadStorage;
+
use radicle_artifact::share::cid_utils;
+
use radicle_artifact::Releases as ReleasesStore;
+

+
use crate::cobs::release;
+
use crate::error::Error;
+
use crate::traits::Profile;
+

+
pub trait Releases: Profile {
+
    /// List every release for a repo, oldest-first by COB timestamp.
+
    fn list_releases(&self, rid: identity::RepoId) -> Result<Vec<release::Release>, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let aliases = profile.aliases();
+
        let our_did = identity::Did::from(profile.public_key);
+

+
        let releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
+

+
        let mut out = Vec::new();
+
        for item in releases.all().map_err(|e| Error::Iroh(e.to_string()))? {
+
            let (id, release) = item.map_err(|e| Error::Iroh(e.to_string()))?;
+
            out.push(release::Release::new(
+
                radicle_artifact::ReleaseId::from(id),
+
                &release,
+
                &our_did,
+
                &aliases,
+
            ));
+
        }
+
        Ok(out)
+
    }
+

+
    fn release_by_id(
+
        &self,
+
        rid: identity::RepoId,
+
        release_id: String,
+
    ) -> Result<Option<release::Release>, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let aliases = profile.aliases();
+
        let our_did = identity::Did::from(profile.public_key);
+

+
        let id = radicle_artifact::ReleaseId::from_str(&release_id)
+
            .map_err(|e| Error::Iroh(format!("invalid release id: {e}")))?;
+
        let releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
+
        let Some(release) = releases.get(&id).map_err(|e| Error::Iroh(e.to_string()))? else {
+
            return Ok(None);
+
        };
+

+
        Ok(Some(release::Release::new(
+
            id, &release, &our_did, &aliases,
+
        )))
+
    }
+

+
    fn releases_by_commit(
+
        &self,
+
        rid: identity::RepoId,
+
        oid: radicle::git::Oid,
+
    ) -> Result<Vec<release::Release>, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let aliases = profile.aliases();
+
        let our_did = identity::Did::from(profile.public_key);
+

+
        let releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
+

+
        let mut out = Vec::new();
+
        for item in releases
+
            .find_by_oid(oid)
+
            .map_err(|e| Error::Iroh(e.to_string()))?
+
        {
+
            let (id, release) = item.map_err(|e| Error::Iroh(e.to_string()))?;
+
            out.push(release::Release::new(id, &release, &our_did, &aliases));
+
        }
+
        Ok(out)
+
    }
+

+
    /// Compute the BLAKE3-derived CID for a local file or directory.
+
    /// Files become Blob CIDs, directories become Collection CIDs.
+
    fn compute_cid(&self, path: PathBuf) -> Result<String, Error> {
+
        let cid = if path.is_dir() {
+
            cid_utils::compute_content_id(&path).map_err(|e| Error::Iroh(e.to_string()))?
+
        } else {
+
            cid_utils::compute_blob_cid(&path).map_err(|e| Error::Iroh(e.to_string()))?
+
        };
+
        Ok(cid.to_string())
+
    }
+
}
added crates/radicle-types/src/traits/release_mut.rs
@@ -0,0 +1,159 @@
+
use std::str::FromStr;
+

+
use radicle::identity;
+
use radicle::storage::ReadStorage;
+
use radicle_artifact::Releases as ReleasesStore;
+
use url::Url;
+

+
use crate::error::Error;
+
use crate::traits::release::Releases;
+

+
pub trait ReleasesMut: Releases {
+
    /// Find a release for the commit OID, or create it. Returns the
+
    /// release id as a string for the frontend.
+
    fn create_or_open_release(
+
        &self,
+
        rid: identity::RepoId,
+
        oid: radicle::git::Oid,
+
    ) -> Result<String, Error> {
+
        let profile = self.profile();
+
        let signer = profile.signer()?;
+
        let repo = profile.storage.repository(rid)?;
+

+
        let mut releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
+
        let release = releases
+
            .find_or_create_by_oid(oid, &signer)
+
            .map_err(|e| Error::Iroh(e.to_string()))?;
+
        Ok(release.id().to_string())
+
    }
+

+
    fn add_artifact(
+
        &self,
+
        rid: identity::RepoId,
+
        release_id: String,
+
        cid: String,
+
        name: String,
+
    ) -> Result<(), Error> {
+
        let profile = self.profile();
+
        let signer = profile.signer()?;
+
        let repo = profile.storage.repository(rid)?;
+

+
        let id = parse_release_id(&release_id)?;
+
        let cid = parse_cid(&cid)?;
+

+
        let mut releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
+
        let mut release = releases
+
            .get_mut(&id)
+
            .map_err(|e| Error::Iroh(e.to_string()))?;
+
        release
+
            .add_artifact(cid, name, &signer)
+
            .map_err(|e| Error::Iroh(e.to_string()))?;
+
        Ok(())
+
    }
+

+
    fn add_location(
+
        &self,
+
        rid: identity::RepoId,
+
        release_id: String,
+
        cid: String,
+
        url: String,
+
    ) -> Result<(), Error> {
+
        let profile = self.profile();
+
        let signer = profile.signer()?;
+
        let repo = profile.storage.repository(rid)?;
+

+
        let id = parse_release_id(&release_id)?;
+
        let cid = parse_cid(&cid)?;
+
        let url = Url::parse(&url).map_err(|e| Error::Iroh(format!("invalid url: {e}")))?;
+

+
        let mut releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
+
        let mut release = releases
+
            .get_mut(&id)
+
            .map_err(|e| Error::Iroh(e.to_string()))?;
+
        release
+
            .add_location(cid, url, &signer)
+
            .map_err(|e| Error::Iroh(e.to_string()))?;
+
        Ok(())
+
    }
+

+
    fn remove_location(
+
        &self,
+
        rid: identity::RepoId,
+
        release_id: String,
+
        cid: String,
+
        url: String,
+
    ) -> Result<(), Error> {
+
        let profile = self.profile();
+
        let signer = profile.signer()?;
+
        let repo = profile.storage.repository(rid)?;
+

+
        let id = parse_release_id(&release_id)?;
+
        let cid = parse_cid(&cid)?;
+
        let url = Url::parse(&url).map_err(|e| Error::Iroh(format!("invalid url: {e}")))?;
+

+
        let mut releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
+
        let mut release = releases
+
            .get_mut(&id)
+
            .map_err(|e| Error::Iroh(e.to_string()))?;
+
        release
+
            .remove_location(cid, url, &signer)
+
            .map_err(|e| Error::Iroh(e.to_string()))?;
+
        Ok(())
+
    }
+

+
    fn attest_artifact(
+
        &self,
+
        rid: identity::RepoId,
+
        release_id: String,
+
        cid: String,
+
    ) -> Result<(), Error> {
+
        let profile = self.profile();
+
        let signer = profile.signer()?;
+
        let repo = profile.storage.repository(rid)?;
+

+
        let id = parse_release_id(&release_id)?;
+
        let cid = parse_cid(&cid)?;
+

+
        let mut releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
+
        let mut release = releases
+
            .get_mut(&id)
+
            .map_err(|e| Error::Iroh(e.to_string()))?;
+
        release
+
            .attest(cid, &signer)
+
            .map_err(|e| Error::Iroh(e.to_string()))?;
+
        Ok(())
+
    }
+

+
    fn redact_artifact(
+
        &self,
+
        rid: identity::RepoId,
+
        release_id: String,
+
        cid: String,
+
        reason: String,
+
    ) -> Result<(), Error> {
+
        let profile = self.profile();
+
        let signer = profile.signer()?;
+
        let repo = profile.storage.repository(rid)?;
+

+
        let id = parse_release_id(&release_id)?;
+
        let cid = parse_cid(&cid)?;
+

+
        let mut releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
+
        let mut release = releases
+
            .get_mut(&id)
+
            .map_err(|e| Error::Iroh(e.to_string()))?;
+
        release
+
            .redact(cid, reason, &signer)
+
            .map_err(|e| Error::Iroh(e.to_string()))?;
+
        Ok(())
+
    }
+
}
+

+
fn parse_release_id(s: &str) -> Result<radicle_artifact::ReleaseId, Error> {
+
    radicle_artifact::ReleaseId::from_str(s)
+
        .map_err(|e| Error::Iroh(format!("invalid release id {s}: {e}")))
+
}
+

+
fn parse_cid(s: &str) -> Result<cid::Cid, Error> {
+
    cid::Cid::from_str(s).map_err(|e| Error::Iroh(format!("invalid cid {s}: {e}")))
+
}