Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
radicle/git/repository: define RefReader, RefWriter, and extension traits
Fintan Halpenny committed 24 days ago
commit 9ecb0f125da006042c22f3094eed13e4e176ca9e
parent 0af7b750737995d9368864df9db36d721e6c5305
5 files changed +377 -0
modified crates/radicle/src/git/repository.rs
@@ -14,4 +14,5 @@ pub mod reference;
pub mod types;

pub use object::{ObjectReader, ObjectWriter};
+
pub use reference::{RefReader, RefTarget, RefWriter, SymbolicRefTarget, SymbolicRefWriter};
pub use types::{Blob, Commit, ObjectKind, TreeEntry};
modified crates/radicle/src/git/repository/reference.rs
@@ -1 +1,215 @@
//! Git reference database abstraction.
+
//!
+
//! The module provides the following traits:
+
//! - [`RefReader`] for reading references,
+
//! - [`RefWriter`] for writing references, and
+
//! - [`SymbolicRefWriter`] (extends [`RefWriter`]) for writing symbolic references.
+

+
pub mod error;
+

+
use radicle_oid::Oid;
+

+
use radicle_git_ref_format::refspec::PatternStr;
+
use radicle_git_ref_format::{Qualified, RefStr, RefString};
+

+
/// Read Git references.
+
///
+
/// # Target Resolution
+
///
+
/// Direct references point to a target [`Oid`]. For most references, this is a
+
/// commit object. In the case of annotated tags, this will be a tag object.
+
/// In both cases, the target returned will be a commit [`Oid`]; where, in the
+
/// case of an annotated tag, it is the commit of the tag itself.
+
///
+
/// Symbolic references point to another reference. These references are peeled
+
/// until they find the target [`Oid`] of a direct reference.
+
pub trait RefReader {
+
    type References<'a>: Iterator<Item = Result<(Qualified<'static>, Oid), error::read::ListReferenceError>>
+
        + 'a
+
    where
+
        Self: 'a;
+

+
    /// Resolve a reference to its target [`Oid`].
+
    ///
+
    /// Returns `None` if the reference does not exist.
+
    ///
+
    /// # Errors
+
    ///
+
    /// - [`Backend`]: An unexpected error from the underlying git library.
+
    ///
+
    /// [`Backend`]: error::read::RefTargetError::Backend
+
    fn ref_target<R>(&self, name: &R) -> Result<Option<Oid>, error::read::RefTargetError>
+
    where
+
        R: AsRef<RefStr>;
+

+
    /// Resolve a reference to its target [`Oid`], returning an error if it does
+
    /// not exist.
+
    ///
+
    /// # Errors
+
    ///
+
    /// - [`NotFound`]: The reference identified by `name` was not found.
+
    /// - [`Backend`]: An unexpected error from the underlying git library.
+
    ///
+
    /// [`NotFound`]: error::read::RefTargetError::NotFound
+
    /// [`Backend`]: error::read::RefTargetError::Backend
+
    fn try_ref_target<R>(&self, name: &R) -> Result<Oid, error::read::RefTargetError>
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        self.ref_target(name)?
+
            .ok_or_else(|| error::read::RefTargetError::NotFound(name.as_ref().to_ref_string()))
+
    }
+

+
    /// List all references matching a glob pattern.
+
    ///
+
    /// Each reference is parsed and peeled to its target commit. If either of
+
    /// these operations fails, it is returned in the iterator. The caller may
+
    /// choose to log these failures and skip the entry.
+
    ///
+
    /// # Errors
+
    ///
+
    /// - [`Backend`]: An unexpected error when initialising the reference
+
    ///   iterator.
+
    ///
+
    /// The iterator itself yields [`ListReferenceError`] for per-reference
+
    /// failures:
+
    /// - [`Parse`]: A reference name could not be parsed as a [`Qualified`].
+
    /// - [`Peel`]: A reference could not be peeled to a target commit.
+
    /// - [`ListReferenceError::Backend`]: An unexpected error during iteration.
+
    ///
+
    /// [`Backend`]: error::read::ListRefsError::Backend
+
    /// [`ListReferenceError`]: error::read::ListReferenceError
+
    /// [`Parse`]: error::read::ListReferenceError::Parse
+
    /// [`Peel`]: error::read::ListReferenceError::Peel
+
    /// [`ListReferenceError::Backend`]: error::read::ListReferenceError::Backend
+
    fn list_refs<'a, P>(
+
        &'a self,
+
        pattern: &P,
+
    ) -> Result<Self::References<'a>, error::read::ListRefsError>
+
    where
+
        P: AsRef<PatternStr>;
+
}
+

+
/// The mode of operation for writing a reference.
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+
pub enum RefTarget {
+
    /// Set the reference to the given `target`, only if the reference does not
+
    /// already exist.
+
    Create { target: Oid },
+
    /// Set the reference to the given `target`, the reference may exist
+
    /// already.
+
    Upsert { target: Oid },
+
    /// Set the reference to the given `target`, only if the reference's
+
    /// current value matches `expected`.
+
    Cas { target: Oid, expected: Oid },
+
}
+

+
impl RefTarget {
+
    /// The target [`Oid`] that the reference should point to after the write.
+
    pub fn target(&self) -> Oid {
+
        match self {
+
            Self::Create { target } | Self::Upsert { target } | Self::Cas { target, .. } => *target,
+
        }
+
    }
+
}
+

+
/// Write Git references.
+
pub trait RefWriter {
+
    /// Set a reference to the given [`RefTarget`].
+
    ///
+
    /// # Errors
+
    ///
+
    /// - [`MissingTarget`]: The target [`Oid`] does not exist in the object
+
    ///   database.
+
    /// - [`ReferenceExists`]: The reference already exists (for
+
    ///   [`RefTarget::Create`]).
+
    /// - [`CasFailed`]: The reference's current value did not match the
+
    ///   expected value (for [`RefTarget::Cas`]).
+
    /// - [`Backend`]: An unexpected error from the underlying git library.
+
    ///
+
    /// [`MissingTarget`]: error::write::WriteRefError::MissingTarget
+
    /// [`ReferenceExists`]: error::write::WriteRefError::ReferenceExists
+
    /// [`CasFailed`]: error::write::WriteRefError::CasFailed
+
    /// [`Backend`]: error::write::WriteRefError::Backend
+
    fn write_ref<R>(
+
        &self,
+
        name: &R,
+
        target: RefTarget,
+
        reflog: &str,
+
    ) -> Result<(), error::write::WriteRefError>
+
    where
+
        R: AsRef<RefStr>;
+

+
    /// Delete a reference from the Git repository.
+
    ///
+
    /// This operation must be idempotent, i.e. successive deletes of the same
+
    /// reference name must succeed.
+
    ///
+
    /// # Errors
+
    ///
+
    /// - [`Backend`]: An unexpected error from the underlying git library.
+
    ///
+
    /// [`Backend`]: error::write::DeleteRefError::Backend
+
    fn delete_ref<R>(&self, name: &R) -> Result<(), error::write::DeleteRefError>
+
    where
+
        R: AsRef<RefStr>;
+
}
+

+
/// The mode of operation for writing a symbolic reference.
+
#[derive(Clone, Debug, PartialEq, Eq)]
+
pub enum SymbolicRefTarget {
+
    /// Set the reference to the given `target`, only if the reference does not
+
    /// already exist.
+
    Create { target: RefString },
+
    /// Set the reference to the given `target`, the reference may exist
+
    /// already.
+
    Upsert { target: RefString },
+
    /// Set the reference to the given `target`, only if the current value of
+
    /// the reference matches `expected`.
+
    Cas {
+
        target: RefString,
+
        expected: RefString,
+
    },
+
}
+

+
impl SymbolicRefTarget {
+
    /// The target [`RefString`] that the symbolic reference should point to
+
    /// after the write.
+
    pub fn target(&self) -> &RefString {
+
        match self {
+
            Self::Create { target } | Self::Upsert { target } | Self::Cas { target, .. } => target,
+
        }
+
    }
+
}
+

+
/// Extension trait for symbolic reference support.
+
///
+
/// A symbolic reference is one that points to another reference name rather
+
/// than directly to an [`Oid`] (e.g. `HEAD → refs/heads/main`).
+
pub trait SymbolicRefWriter: RefWriter {
+
    /// Create or update a symbolic reference, identified by `name`, with the
+
    /// given [`SymbolicRefTarget`].
+
    ///
+
    /// # Errors
+
    ///
+
    /// - [`MissingTarget`]: The target reference does not exist in the
+
    ///   reference database.
+
    /// - [`ReferenceExists`]: The symbolic reference `name` already exists
+
    ///   (for [`SymbolicRefTarget::Create`]).
+
    /// - [`CasFailed`]: The current target did not match the expected value
+
    ///   (for [`SymbolicRefTarget::Cas`]).
+
    /// - [`Backend`]: An unexpected error from the underlying git library.
+
    ///
+
    /// [`MissingTarget`]: error::write::SymbolicWriteError::MissingTarget
+
    /// [`ReferenceExists`]: error::write::SymbolicWriteError::ReferenceExists
+
    /// [`CasFailed`]: error::write::SymbolicWriteError::CasFailed
+
    /// [`Backend`]: error::write::SymbolicWriteError::Backend
+
    fn write_symbolic_ref<R>(
+
        &self,
+
        name: &R,
+
        target: SymbolicRefTarget,
+
        reflog: &str,
+
    ) -> Result<(), error::write::SymbolicWriteError>
+
    where
+
        R: AsRef<RefStr>;
+
}
added crates/radicle/src/git/repository/reference/error.rs
@@ -0,0 +1,4 @@
+
//! Errors for Git reference operations, namespaced by read and write.
+

+
pub mod read;
+
pub mod write;
added crates/radicle/src/git/repository/reference/error/read.rs
@@ -0,0 +1,72 @@
+
//! Errors returned by [`RefReader`] methods.
+
//!
+
//! [`RefReader`]: super::super::RefReader
+

+
use radicle_git_ref_format::RefString;
+
use thiserror::Error;
+

+
/// Error returned by [`RefReader::ref_target`].
+
///
+
/// [`RefReader::ref_target`]: super::super::RefReader::ref_target
+
#[derive(Debug, Error)]
+
#[non_exhaustive]
+
pub enum RefTargetError {
+
    /// The requested reference was not found.
+
    #[error("failed to find reference '{0}'")]
+
    NotFound(RefString),
+
    /// An error from the underlying git library.
+
    #[error(transparent)]
+
    Backend(Box<dyn std::error::Error + Send + Sync + 'static>),
+
}
+

+
impl RefTargetError {
+
    pub fn backend<E: std::error::Error + Send + Sync + 'static>(err: E) -> Self {
+
        Self::Backend(Box::new(err))
+
    }
+
}
+

+
/// Error returned by [`RefReader::list_refs`].
+
///
+
/// [`RefReader::list_refs`]: super::super::RefReader::list_refs
+
#[derive(Debug, Error)]
+
#[non_exhaustive]
+
pub enum ListRefsError {
+
    /// An error from the underlying git library.
+
    #[error(transparent)]
+
    Backend(Box<dyn std::error::Error + Send + Sync + 'static>),
+
}
+

+
impl ListRefsError {
+
    pub fn backend<E: std::error::Error + Send + Sync + 'static>(err: E) -> Self {
+
        Self::Backend(Box::new(err))
+
    }
+
}
+

+
/// Error yielded by the [`RefReader::list_refs`] iterator.
+
///
+
/// [`RefReader::list_refs`]: super::super::RefReader::list_refs
+
#[derive(Debug, Error)]
+
#[non_exhaustive]
+
pub enum ListReferenceError {
+
    /// The reference database provided a malformed reference name.
+
    #[error("failed to parse reference '{name}': {source}")]
+
    Parse {
+
        name: String,
+
        source: radicle_git_ref_format::Error,
+
    },
+
    /// The reference could not be peeled to a target commit.
+
    #[error("failed to peel '{name}' to target commit: {source}")]
+
    Peel {
+
        name: radicle_git_ref_format::Qualified<'static>,
+
        source: Box<dyn std::error::Error + Send + Sync + 'static>,
+
    },
+
    /// An error from the underlying git library.
+
    #[error(transparent)]
+
    Backend(Box<dyn std::error::Error + Send + Sync + 'static>),
+
}
+

+
impl ListReferenceError {
+
    pub fn backend<E: std::error::Error + Send + Sync + 'static>(err: E) -> Self {
+
        Self::Backend(Box::new(err))
+
    }
+
}
added crates/radicle/src/git/repository/reference/error/write.rs
@@ -0,0 +1,86 @@
+
//! Errors returned by [`RefWriter`] and [`SymbolicRefWriter`] methods.
+
//!
+
//! [`RefWriter`]: super::super::RefWriter
+
//! [`SymbolicRefWriter`]: super::super::SymbolicRefWriter
+

+
use radicle_git_ref_format::RefString;
+
use radicle_oid::Oid;
+
use thiserror::Error;
+

+
/// Error returned by [`RefWriter::write_ref`].
+
///
+
/// [`RefWriter::write_ref`]: super::super::RefWriter::write_ref
+
#[derive(Debug, Error)]
+
#[non_exhaustive]
+
pub enum WriteRefError {
+
    /// Compare-and-swap failed.
+
    #[error(
+
        "failed to update reference '{name}' due to compare-and-swap failure with expected value {expected}"
+
    )]
+
    CasFailed { name: String, expected: Oid },
+
    /// The target OID does not exist in the object database.
+
    #[error("target object {target} not found when writing reference `{name}`")]
+
    MissingTarget { name: String, target: Oid },
+
    /// The reference already exists (for create-only writes).
+
    #[error("reference '{name}' already exists")]
+
    ReferenceExists { name: String },
+
    /// An error from the underlying git library.
+
    #[error(transparent)]
+
    Backend(Box<dyn std::error::Error + Send + Sync + 'static>),
+
}
+

+
impl WriteRefError {
+
    pub fn backend<E: std::error::Error + Send + Sync + 'static>(err: E) -> Self {
+
        Self::Backend(Box::new(err))
+
    }
+
}
+

+
/// Error returned by [`RefWriter::delete_ref`].
+
///
+
/// [`RefWriter::delete_ref`]: super::super::RefWriter::delete_ref
+
#[derive(Debug, Error)]
+
#[non_exhaustive]
+
pub enum DeleteRefError {
+
    /// An error from the underlying git library.
+
    #[error(transparent)]
+
    Backend(Box<dyn std::error::Error + Send + Sync + 'static>),
+
}
+

+
impl DeleteRefError {
+
    pub fn backend<E: std::error::Error + Send + Sync + 'static>(err: E) -> Self {
+
        Self::Backend(Box::new(err))
+
    }
+
}
+

+
/// Error returned by [`SymbolicRefWriter::write_symbolic_ref`].
+
///
+
/// [`SymbolicRefWriter::write_symbolic_ref`]: super::super::SymbolicRefWriter::write_symbolic_ref
+
#[derive(Debug, Error)]
+
#[non_exhaustive]
+
pub enum SymbolicWriteError {
+
    /// The target reference does not exist.
+
    #[error("could not create symbolic reference '{name}' due to missing target '{target}'")]
+
    MissingTarget { name: RefString, target: RefString },
+
    /// The named reference already exists.
+
    #[error(
+
        "could not create symbolic reference from '{name}' to '{target}', the reference already exists"
+
    )]
+
    ReferenceExists { name: RefString, target: RefString },
+
    /// Compare-and-swap failed.
+
    #[error(
+
        "failed to update reference '{name}' due to compare-and-swap failure with expected value {expected}"
+
    )]
+
    CasFailed {
+
        name: RefString,
+
        expected: RefString,
+
    },
+
    /// An error from the underlying git library.
+
    #[error(transparent)]
+
    Backend(Box<dyn std::error::Error + Send + Sync + 'static>),
+
}
+

+
impl SymbolicWriteError {
+
    pub fn backend<E: std::error::Error + Send + Sync + 'static>(err: E) -> Self {
+
        Self::Backend(Box::new(err))
+
    }
+
}