radicle-ci-broker 0.24.0

add integration to CI engins or systems to a Radicle node
Documentation
//! Git reference names, namespaced and not.

use std::str::FromStr;

use serde::{Deserialize, Serialize};

use radicle::{
    cob::patch::PatchId,
    git::{BranchName, Component, Namespaced, Qualified, RefStr, RefString},
};

/// A generic ref name.
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
pub struct GenericRefName(RefString);

impl GenericRefName {
    pub fn as_str(&self) -> &str {
        self.as_ref()
    }
}

impl AsRef<str> for GenericRefName {
    fn as_ref(&self) -> &str {
        self.0.as_str()
    }
}

impl From<Qualified<'_>> for GenericRefName {
    fn from(from: Qualified<'_>) -> Self {
        Self::from(&from)
    }
}

impl From<&Qualified<'_>> for GenericRefName {
    fn from(from: &Qualified<'_>) -> Self {
        Self(from.to_ref_string())
    }
}

/// A tag name.
///
/// This is exactly like a [`RefString`], but meant to have stronger
/// type safety as a separate type. It's also more self-documenting.
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
pub struct TagName(RefString);

impl TagName {
    pub fn starts_with(&self, s: &str) -> bool {
        self.as_ref().starts_with(s)
    }

    pub fn as_str(&self) -> &str {
        self.as_ref()
    }
}

impl TryFrom<&str> for TagName {
    type Error = RefError;
    fn try_from(from: &str) -> Result<Self, RefError> {
        Ok(Self(RefString::try_from(from).map_err(|err| {
            RefError::RefStrCreate(from.to_string(), err)
        })?))
    }
}

impl From<RefString> for TagName {
    fn from(from: RefString) -> Self {
        Self(from)
    }
}

impl AsRef<str> for TagName {
    fn as_ref(&self) -> &str {
        self.0.as_str()
    }
}

/// Convert a plain branch name (`main`) from a Git ref
pub fn branch_ref(name: &RefStr) -> Result<BranchName, RefError> {
    if name.starts_with("refs/") {
        return Err(RefError::RefsInName(name.to_ref_string()));
    }
    ref_string(name)
}

/// Create a [`BranchName`] from a string slice.
pub fn branch_from_str(s: &str) -> Result<BranchName, RefError> {
    branch_ref(&ref_string(s)?)
}

/// Convert a branch name to a [`Qualified`]: `refs/heads/main`.
pub fn qualified_branch(name: &BranchName) -> Result<Qualified<'_>, RefError> {
    if name.starts_with("refs/") {
        return Err(RefError::RefsInName(name.to_ref_string()));
    }
    let qualified_name = ref_string(&format!("refs/heads/{name}"))?;
    let x = Qualified::from_refstr(qualified_name);
    x.ok_or(RefError::QualifiedCreate(name.clone()))
}

/// Create a [`RefString`] from a [`String`].
pub fn ref_string(s: &str) -> Result<RefString, RefError> {
    RefString::try_from(s).map_err(|err| RefError::RefStrCreate(s.into(), err))
}

/// Create a name spaced branch name.
pub fn namespaced_branch<'a>(
    ns: &RefStr,
    branch: &'a BranchName,
) -> Result<Namespaced<'a>, RefError> {
    let ns = Component::from_refstr(ns).ok_or(RefError::NamespaceName(ns.to_ref_string()))?;
    Ok(qualified_branch(branch)?.with_namespace(ns))
}

/// Create a name spaced ref name from a string slice.
pub fn namespaced_from_str(s: &str) -> Result<Namespaced<'_>, RefError> {
    assert!(s.starts_with("refs/namespaces/"));
    let rs = ref_string(s)?;
    Ok(rs
        .to_namespaced()
        .ok_or(RefError::NamespacedCreate(s.into()))?
        .to_owned())
}

/// Extract a [`BranchName`] from a name spaced branch.
pub fn branch_from_namespaced(ns: &Namespaced) -> Result<BranchName, RefError> {
    let plain_ref = ns.strip_namespace();

    let (refs, heads, first, rest) = plain_ref.non_empty_components();
    if refs.as_str() != "refs" || heads.as_str() != "heads" {
        return Err(RefError::NotABranch(ns.to_ref_string()));
    }
    Ok(BranchName::from_iter(
        [first.into_inner().to_ref_string()]
            .iter()
            .cloned()
            .chain(rest.map(|c| c.into_inner().to_ref_string())),
    ))
}

/// Create a [`PatchId`] from a string slice.
pub fn patch_from_str(s: &str) -> Result<PatchId, RefError> {
    PatchId::from_str(s).map_err(|_| RefError::PatchIdFromStr(s.into()))
}

/// All errors from Git reference manipulation.
#[derive(Debug, thiserror::Error, Eq, PartialEq)]
pub enum RefError {
    /// Branch name starts with "refs/", but a plain name is wanted.
    #[error("programming error: plain branch name must not start with refs/: {0:?}")]
    RefsInName(RefString),

    /// Failed to create a [`RefString`] from a string.
    #[error("failed to create a RefStr from a string {0:?}")]
    RefStrCreate(String, #[source] radicle::git::fmt::Error),

    /// Can't create a [`Qualified`] value from a [`BranchName`].
    #[error("failed to create a qualified branch name from a branch ref {0:?}")]
    QualifiedCreate(BranchName),

    /// Can't create a [`Namespaced`] value from a string slice.
    #[error("failed to create a name spaced Git ref from {0:?}")]
    NamespacedCreate(String),

    /// Name spaced branch name does not start with `refs/heads`".
    #[error("failed to get branch name from {0:?})")]
    NotABranch(RefString),

    /// Can't create a [`Component`] from a name space name.
    #[error("failed to create a name space component from its name {0:?}")]
    NamespaceName(RefString),

    /// Can't create a patch id from string.
    #[error("failed to create a patch id from string: {0:?}")]
    PatchIdFromStr(String),
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
    use std::str::FromStr;

    use super::*;

    #[test]
    fn ref_string_from_plain_branch_name() {
        assert_eq!(ref_string("main").map(|x| x.to_string()), Ok("main".into()));
    }

    #[test]
    fn plain_branch_name() {
        assert_eq!(
            branch_ref(&ref_string("main").unwrap()).map(|x| x.to_string()),
            Ok("main".into())
        );
    }

    #[test]
    fn qualified_branch_name_from_plain() {
        let name = branch_ref(&ref_string("main").unwrap()).unwrap();
        assert_eq!(
            qualified_branch(&name).map(|x| x.to_string()),
            Ok("refs/heads/main".into())
        );
    }

    #[test]
    fn namespaced_branch_from_plain() {
        let branch = branch_ref(&ref_string("main").unwrap()).unwrap();
        let ns = ref_string("node1").unwrap();
        let name = namespaced_branch(&ns, &branch);
        assert_eq!(
            name.map(|x| x.to_string()),
            Ok("refs/namespaces/node1/refs/heads/main".into())
        );
    }

    #[test]
    fn extracts_branch_namespaced_branch() {
        let branch = branch_ref(&ref_string("main").unwrap()).unwrap();
        let ns = ref_string("node1").unwrap();
        let name = namespaced_branch(&ns, &branch).unwrap();
        let extracted = branch_from_namespaced(&name);
        assert_eq!(extracted.map(|x| x.to_string()), Ok("main".into()));
    }

    #[test]
    fn creates_patch_from_str() {
        let oid = PatchId::from_str("e76d814f6934f24d45f628f8ff9533dcdefc1bd8").unwrap();
        assert_eq!(
            patch_from_str("e76d814f6934f24d45f628f8ff9533dcdefc1bd8"),
            Ok(oid)
        );
    }
}