Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle-git-metadata src commit.rs
pub mod headers;
pub mod trailers;

mod parse;
pub use parse::ParseError;

use core::fmt;
use std::str::{self, FromStr};

use headers::{Headers, Signature};
use trailers::{OwnedTrailer, Trailer};

use crate::author::Author;

/// A git commit in its object description form, i.e. the output of
/// `git cat-file` for a commit object.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct CommitData<Tree, Parent> {
    tree: Tree,
    parents: Vec<Parent>,
    author: Author,
    committer: Author,
    headers: Headers,
    message: String,
    trailers: Vec<OwnedTrailer>,
}

impl<Tree, Parent> CommitData<Tree, Parent> {
    pub fn new<P, I, T>(
        tree: Tree,
        parents: P,
        author: Author,
        committer: Author,
        headers: Headers,
        message: String,
        trailers: I,
    ) -> Self
    where
        P: IntoIterator<Item = Parent>,
        I: IntoIterator<Item = T>,
        OwnedTrailer: From<T>,
    {
        let trailers = trailers.into_iter().map(OwnedTrailer::from).collect();
        let parents = parents.into_iter().collect();
        Self {
            tree,
            parents,
            author,
            committer,
            headers,
            message,
            trailers,
        }
    }

    /// The tree this commit points to.
    pub fn tree(&self) -> &Tree {
        &self.tree
    }

    /// The parents of this commit.
    pub fn parents(&self) -> impl Iterator<Item = Parent> + '_
    where
        Parent: Clone,
    {
        self.parents.iter().cloned()
    }

    /// The author of this commit, i.e. the header corresponding to `author`.
    pub fn author(&self) -> &Author {
        &self.author
    }

    /// The committer of this commit, i.e. the header corresponding to
    /// `committer`.
    pub fn committer(&self) -> &Author {
        &self.committer
    }

    /// The message body of this commit.
    pub fn message(&self) -> &str {
        &self.message
    }

    /// The [`Signature`]s found in this commit, i.e. the headers corresponding
    /// to `gpgsig`.
    pub fn signatures(&self) -> impl Iterator<Item = Signature<'_>> + '_ {
        self.headers.signatures()
    }

    pub fn strip_signatures(mut self) -> Self {
        self.headers.strip_signatures();
        self
    }

    /// The [`Headers`] found in this commit.
    ///
    /// Note: these do not include `tree`, `parent`, `author`, and `committer`.
    pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
        self.headers.iter()
    }

    /// Iterate over the [`Headers`] values that match the provided `name`.
    pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + 'a {
        self.headers.values(name)
    }

    /// Push a header to the end of the headers section.
    pub fn push_header(&mut self, name: &str, value: &str) {
        self.headers.push(name, value.trim());
    }

    pub fn trailers(&self) -> impl Iterator<Item = &OwnedTrailer> {
        self.trailers.iter()
    }

    /// Convert the `CommitData::tree` into a value of type `U`. The
    /// conversion function `f` can be fallible.
    ///
    /// For example, `map_tree` can be used to turn raw tree data into
    /// an `Oid` by writing it to a repository.
    pub fn map_tree<U, E, F>(self, f: F) -> Result<CommitData<U, Parent>, E>
    where
        F: FnOnce(Tree) -> Result<U, E>,
    {
        Ok(CommitData {
            tree: f(self.tree)?,
            parents: self.parents,
            author: self.author,
            committer: self.committer,
            headers: self.headers,
            message: self.message,
            trailers: self.trailers,
        })
    }

    /// Convert the [`CommitData::parents`] into a vector containing
    /// values of type `U`. The conversion function `f` can be
    /// fallible.
    ///
    /// For example, this can be used to resolve the object identifiers
    /// to their respective full commits.
    pub fn map_parents<U, E, F>(self, f: F) -> Result<CommitData<Tree, U>, E>
    where
        F: FnMut(Parent) -> Result<U, E>,
    {
        Ok(CommitData {
            tree: self.tree,
            parents: self
                .parents
                .into_iter()
                .map(f)
                .collect::<Result<Vec<_>, _>>()?,
            author: self.author,
            committer: self.committer,
            headers: self.headers,
            message: self.message,
            trailers: self.trailers,
        })
    }
}

impl<Tree, Parent> CommitData<Tree, Parent>
where
    Tree: str::FromStr,
    Parent: str::FromStr,
    Tree::Err: std::error::Error + Send + Sync + 'static,
    Parent::Err: std::error::Error + Send + Sync + 'static,
{
    /// Parse a [`CommitData`] from its raw git object bytes.
    ///
    /// This is the inverse of the [`fmt::Display`] implementation. The bytes
    /// are expected to be valid UTF-8 and in the standard git commit object
    /// format produced by `git cat-file -p <commit>`.
    ///
    /// Trailers are detected by scanning the last paragraph of the message
    /// body (the section after the final blank line). If every non-empty line
    /// in that paragraph is a valid `Token: value` pair, those lines are
    /// parsed as trailers and stored separately; otherwise the whole body is
    /// kept as the message with no trailers.
    pub fn from_bytes(bytes: &[u8]) -> Result<Self, ParseError> {
        let s = str::from_utf8(bytes).map_err(ParseError::Utf8)?;
        parse::parse(s)
    }
}

impl<Tree, Parent> FromStr for CommitData<Tree, Parent>
where
    Tree: str::FromStr,
    Parent: str::FromStr,
    Tree::Err: std::error::Error + Send + Sync + 'static,
    Parent::Err: std::error::Error + Send + Sync + 'static,
{
    type Err = ParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        parse::parse(s)
    }
}

impl<Tree: fmt::Display, Parent: fmt::Display> fmt::Display for CommitData<Tree, Parent> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "tree {}", self.tree)?;
        for parent in self.parents.iter() {
            writeln!(f, "parent {parent}")?;
        }
        writeln!(f, "author {}", self.author)?;
        writeln!(f, "committer {}", self.committer)?;

        for (name, value) in self.headers.iter() {
            writeln!(f, "{name} {}", value.replace('\n', "\n "))?;
        }
        writeln!(f)?;
        write!(f, "{}", self.message.trim())?;
        writeln!(f)?;

        if !self.trailers.is_empty() {
            writeln!(f)?;
        }
        for trailer in self.trailers.iter() {
            writeln!(f, "{}", Trailer::from(trailer).display(": "))?;
        }
        Ok(())
    }
}