radicle-ci-broker 0.24.0

add integration to CI engins or systems to a Radicle node
Documentation
use std::{
    fs::Permissions,
    io::Write,
    os::unix::fs::PermissionsExt,
    path::{Path, PathBuf},
    str::FromStr,
};

use tempfile::NamedTempFile;
use time::{
    OffsetDateTime, PrimitiveDateTime,
    format_description::{FormatItem, well_known::Rfc2822},
    macros::format_description,
    parsing::Parsable,
};

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

pub fn lookup_repo(profile: &Profile, wanted: &str) -> Result<(RepoId, String), UtilError> {
    let storage = Storage::open(profile.storage(), profile.info()).map_err(UtilError::Storage)?;

    let repos = storage.repositories().map_err(UtilError::Repositories)?;
    let mut rid = None;

    if let Ok(wanted_rid) = RepoId::from_urn(wanted) {
        for ri in repos {
            let project = ri
                .doc
                .project()
                .map_err(|e| UtilError::Project(ri.rid, e))?;

            if ri.rid == wanted_rid {
                if rid.is_some() {
                    return Err(UtilError::DuplicateRepositories(wanted.into()));
                }
                rid = Some((ri.rid, project.name().to_string()));
            }
        }
    } else {
        for ri in repos {
            let project = ri
                .doc
                .project()
                .map_err(|e| UtilError::Project(ri.rid, e))?;

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

    if let Some(rid) = rid {
        Ok(rid)
    } else {
        Err(UtilError::NotFound(wanted.into()))
    }
}

pub fn oid_from_cli_arg(profile: &Profile, rid: RepoId, commit: &str) -> Result<Oid, UtilError> {
    if let Ok(oid) = Oid::from_str(commit) {
        Ok(oid)
    } else {
        lookup_commit(profile, rid, commit)
    }
}

pub fn load_profile() -> Result<Profile, UtilError> {
    Profile::load().map_err(UtilError::Profile)
}

pub fn lookup_nid(profile: &Profile) -> Result<NodeId, UtilError> {
    Ok(*profile.id())
}

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

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

pub fn now() -> Result<String, UtilError> {
    let fmt = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]Z");
    OffsetDateTime::now_utc()
        .format(fmt)
        .map_err(UtilError::TimeFormat)
}

pub fn parse_timestamp(timestamp: &str) -> Result<OffsetDateTime, UtilError> {
    const SIMPLIFIED_ISO8601_WITH_Z: &[FormatItem<'static>] =
        format_description!("[year]-[month]-[day] [hour]:[minute]:[second]Z");

    fn parse_one(
        timestamp: &str,
        fmt: &(impl Parsable + ?Sized),
    ) -> Result<OffsetDateTime, time::error::Parse> {
        let r = PrimitiveDateTime::parse(timestamp, fmt);
        if let Ok(t) = r {
            Ok(t.assume_utc())
        } else {
            #[allow(clippy::unwrap_used)]
            Err(r.err().unwrap())
        }
    }

    if let Ok(t) = parse_one(timestamp, SIMPLIFIED_ISO8601_WITH_Z) {
        Ok(t)
    } else {
        Err(UtilError::TimestampParse(timestamp.into()))
    }
}

pub fn rfc822_timestamp(ts: &OffsetDateTime) -> Result<String, UtilError> {
    let ts = ts.format(&Rfc2822).map_err(UtilError::TimeFormat)?;
    Ok(ts.to_string())
}

pub fn read_file_as_string(filename: &Path) -> Result<String, UtilError> {
    String::from_utf8(
        std::fs::read(filename).map_err(|err| UtilError::Readfile(filename.into(), err))?,
    )
    .map_err(|err| UtilError::Utf8(filename.into(), err))
}

pub fn read_file_as_objectid(filename: &Path) -> Result<ObjectId, UtilError> {
    let s = read_file_as_string(filename)?;
    ObjectId::from_str(s.trim()).map_err(|err| UtilError::ReadObjectId(filename.into(), err))
}

pub fn safely_overwrite<P: AsRef<Path>>(filename: P, data: &[u8]) -> Result<(), UtilError> {
    let filename = filename.as_ref();
    let dirname = filename
        .parent()
        .ok_or(UtilError::NoParent(filename.to_path_buf()))?;
    let mut tmp = NamedTempFile::new_in(dirname)
        .map_err(|err| UtilError::CreateTemp(dirname.to_path_buf(), err))?;
    tmp.write_all(data)
        .map_err(|err| UtilError::WriteTemp(dirname.to_path_buf(), err))?;
    let mode = Permissions::from_mode(0o644);
    std::fs::set_permissions(tmp.path(), mode).map_err(UtilError::TempPerm)?;
    tmp.persist(filename)
        .map_err(|err| UtilError::RenameTemp(filename.to_path_buf(), err))?;
    Ok(())
}

#[derive(Debug, thiserror::Error)]
pub enum UtilError {
    #[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("failed to open git repository in node storage: {0}")]
    RepoOpen(RepoId, #[source] radicle::storage::RepositoryError),

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

    #[error("failed to format time stamp")]
    TimeFormat(#[source] time::error::Format),

    #[error("failed to parse timestamp {0:?}")]
    TimestampParse(String),

    #[error("failed to read file {0}")]
    Readfile(PathBuf, #[source] std::io::Error),

    #[error("failed to convert file to UTF8: {0}")]
    Utf8(PathBuf, #[source] std::string::FromUtf8Error),

    #[error("failed to read object id from {0}")]
    ReadObjectId(PathBuf, #[source] radicle::cob::object::ParseObjectId),

    #[error("file name to write to doesn't have a parent directory: {0}")]
    NoParent(PathBuf),

    #[error("failed to create temporary file in directory {0}")]
    CreateTemp(PathBuf, #[source] std::io::Error),

    #[error("failed to write to temporary file in {0}")]
    WriteTemp(PathBuf, #[source] std::io::Error),

    #[error("failed to set permissions on temporary file")]
    TempPerm(#[source] std::io::Error),

    #[error("failed to rename temporary file to {0}")]
    RenameTemp(PathBuf, #[source] tempfile::PersistError),
}