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: Tree, parents: Vec, author: Author, committer: Author, headers: Headers, message: String, trailers: Vec, } impl CommitData { pub fn new( tree: Tree, parents: P, author: Author, committer: Author, headers: Headers, message: String, trailers: I, ) -> Self where P: IntoIterator, I: IntoIterator, OwnedTrailer: From, { 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 + '_ 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> + '_ { 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 { self.headers.iter() } /// Iterate over the [`Headers`] values that match the provided `name`. pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator + '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 { 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(self, f: F) -> Result, E> where F: FnOnce(Tree) -> Result, { 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(self, f: F) -> Result, E> where F: FnMut(Parent) -> Result, { Ok(CommitData { tree: self.tree, parents: self .parents .into_iter() .map(f) .collect::, _>>()?, author: self.author, committer: self.committer, headers: self.headers, message: self.message, trailers: self.trailers, }) } } impl CommitData 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 `. /// /// 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 { let s = str::from_utf8(bytes).map_err(ParseError::Utf8)?; parse::parse(s) } } impl FromStr for CommitData 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 { parse::parse(s) } } impl fmt::Display for CommitData { 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(()) } }