diff --git a/crates/mono-repository/src/repository.rs b/crates/mono-repository/src/repository.rs index a6cbbe7..3649764 100644 --- a/crates/mono-repository/src/repository.rs +++ b/crates/mono-repository/src/repository.rs @@ -30,6 +30,7 @@ use std::path::Path; use std::process::Command; pub mod commit; +pub mod commits; mod error; pub mod id; pub mod versions; diff --git a/crates/mono-repository/src/repository/commit.rs b/crates/mono-repository/src/repository/commit.rs index a8cf864..5e65b27 100644 --- a/crates/mono-repository/src/repository/commit.rs +++ b/crates/mono-repository/src/repository/commit.rs @@ -32,10 +32,12 @@ use super::id::Id; use super::Repository; mod delta; -mod iter; +mod deltas; +mod trailers; -pub use delta::{Delta, Deltas}; -pub use iter::Commits; +pub use delta::Delta; +pub use deltas::Deltas; +pub use trailers::Trailers; // ---------------------------------------------------------------------------- // Structs @@ -166,10 +168,7 @@ impl fmt::Debug for Commit<'_> { /// /// [`Error::Git`]: crate::repository::Error::Git pub fn trim_trailers(message: &str) -> Result<&str> { - // We must add two line feeds to the message, or the trailers would not be - // discoverable, since git assumes that we pass the entire commit message - let prepared = format!("\n\n{message}"); - let trailers = git2::message_trailers_strs(prepared.as_str())?; + let trailers: Trailers = message.parse()?; if let Some((key, _)) = trailers.iter().next() { Ok(message.split_once(key).map_or(message, |(body, _)| body)) } else { diff --git a/crates/mono-repository/src/repository/commit/delta.rs b/crates/mono-repository/src/repository/commit/delta.rs index 5de4125..23b44cb 100644 --- a/crates/mono-repository/src/repository/commit/delta.rs +++ b/crates/mono-repository/src/repository/commit/delta.rs @@ -27,10 +27,6 @@ use std::path::PathBuf; -mod iter; - -pub use iter::Deltas; - // ---------------------------------------------------------------------------- // Enums // ---------------------------------------------------------------------------- diff --git a/crates/mono-repository/src/repository/commit/delta/iter.rs b/crates/mono-repository/src/repository/commit/deltas.rs similarity index 99% rename from crates/mono-repository/src/repository/commit/delta/iter.rs rename to crates/mono-repository/src/repository/commit/deltas.rs index 83d012b..328e898 100644 --- a/crates/mono-repository/src/repository/commit/delta/iter.rs +++ b/crates/mono-repository/src/repository/commit/deltas.rs @@ -28,7 +28,7 @@ use crate::repository::commit::Commit; use crate::repository::Result; -use super::Delta; +use super::delta::Delta; // ---------------------------------------------------------------------------- // Structs diff --git a/crates/mono-repository/src/repository/commit/trailers.rs b/crates/mono-repository/src/repository/commit/trailers.rs new file mode 100644 index 0000000..9de68e0 --- /dev/null +++ b/crates/mono-repository/src/repository/commit/trailers.rs @@ -0,0 +1,151 @@ +// Copyright (c) 2025 Zensical and contributors + +// SPDX-License-Identifier: MIT +// Third-party contributions licensed under DCO + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +// ---------------------------------------------------------------------------- + +//! Commit trailer set. + +use std::fmt; +use std::str::FromStr; + +use crate::repository::commit::Commit; +use crate::repository::{Error, Result}; + +// ---------------------------------------------------------------------------- +// Structs +// ---------------------------------------------------------------------------- + +/// Commit trailer set. +pub struct Trailers { + /// Git trailers. + inner: git2::MessageTrailersStrs, +} + +// ---------------------------------------------------------------------------- +// Implementations +// ---------------------------------------------------------------------------- + +impl Commit<'_> { + /// Returns the trailer set of the commit. + /// + /// We must add two line feeds to the message, or the trailers would not be + /// discoverable, since git assumes that we pass the entire commit message + /// + /// # Errors + /// + /// This method returns [`Error::Git`] if the operation fails. + #[allow(clippy::missing_panics_doc)] + #[inline] + pub fn trailers(&self) -> Result { + Trailers::from_str(self.inner.body().unwrap_or_default()) + } +} + +// ---------------------------------------------------------------------------- + +impl Trailers { + /// Returns a reference to the value identified by the key. + pub fn get(&self, key: K) -> Option<&str> + where + K: AsRef, + { + let mut iter = self.inner.iter(); + iter.find_map(|(candidate, value)| { + (candidate == key.as_ref()).then_some(value) + }) + } + + /// Returns whether the commit trailer set contains the key. + pub fn contains_key(&self, key: K) -> bool + where + K: AsRef, + { + let mut iter = self.inner.iter(); + iter.any(|(candidate, _)| candidate == key.as_ref()) + } + + /// Creates an iterator over the commit trailer set. + #[inline] + #[must_use] + pub fn iter(&self) -> git2::MessageTrailersStrsIterator<'_> { + self.into_iter() + } +} + +#[allow(clippy::must_use_candidate)] +impl Trailers { + /// Returns the number of trailers. + #[inline] + pub fn len(&self) -> usize { + self.inner.len() + } + + /// Returns whether there are any trailers. + #[inline] + pub fn is_empty(&self) -> bool { + self.inner.len() == 0 + } +} + +// ---------------------------------------------------------------------------- +// Trait implementations +// ---------------------------------------------------------------------------- + +impl FromStr for Trailers { + type Err = Error; + + /// Attempts to create a commit trailer set from a string. + /// + /// # Errors + /// + /// This method returns [`Error::Git`] if the operation fails. + fn from_str(value: &str) -> Result { + // We must add two line feeds to the message or the trailers wouldn't be + // discoverable, as git assumes that we pass the entire commit message + let prepared = format!("\n\n{value}"); + Ok(Self { + inner: git2::message_trailers_strs(prepared.as_str())?, + }) + } +} + +// ---------------------------------------------------------------------------- + +impl<'a> IntoIterator for &'a Trailers { + type Item = (&'a str, &'a str); + type IntoIter = git2::MessageTrailersStrsIterator<'a>; + + /// Creates an iterator over the commit trailer set. + fn into_iter(self) -> Self::IntoIter { + self.inner.iter() + } +} + +// ---------------------------------------------------------------------------- + +impl fmt::Debug for Trailers { + /// Formats the commit trailer set for debugging. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_map().entries(self).finish() + } +} diff --git a/crates/mono-repository/src/repository/commit/iter.rs b/crates/mono-repository/src/repository/commits.rs similarity index 99% rename from crates/mono-repository/src/repository/commit/iter.rs rename to crates/mono-repository/src/repository/commits.rs index da0b637..5f7eb07 100644 --- a/crates/mono-repository/src/repository/commit/iter.rs +++ b/crates/mono-repository/src/repository/commits.rs @@ -30,7 +30,7 @@ use std::ops::{Bound, RangeBounds}; use crate::repository::id::Id; use crate::repository::{Error, Repository, Result}; -use super::Commit; +use super::commit::Commit; // ---------------------------------------------------------------------------- // Structs diff --git a/crates/mono-repository/src/repository/id.rs b/crates/mono-repository/src/repository/id.rs index f314493..2bae923 100644 --- a/crates/mono-repository/src/repository/id.rs +++ b/crates/mono-repository/src/repository/id.rs @@ -80,7 +80,7 @@ impl From for Id { impl fmt::Display for Id { /// Formats the object identifier for display. - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.0.fmt(f) } } diff --git a/crates/mono-repository/src/repository/versions.rs b/crates/mono-repository/src/repository/versions.rs index bae364c..e279047 100644 --- a/crates/mono-repository/src/repository/versions.rs +++ b/crates/mono-repository/src/repository/versions.rs @@ -32,7 +32,7 @@ use std::fmt; use std::iter::Rev; use std::ops::RangeBounds; -use super::commit::Commits; +use super::commits::Commits; use super::error::{Error, Result}; use super::id::Id; use super::Repository;