diff --git a/git-commit/Cargo.toml b/git-commit/Cargo.toml index 4deba1f..1ae0ff5 100644 --- a/git-commit/Cargo.toml +++ b/git-commit/Cargo.toml @@ -8,7 +8,7 @@ authors = [ "Fintan Halpenny ", ] description = "A small library for parsing, displaying and creating a git commit" -keywords = ["git", "radicle"] +keywords = ["git", "git-commit", "git-trailers", "radicle"] [dependencies] thiserror = "1" @@ -17,7 +17,3 @@ thiserror = "1" version = "0.16.1" default-features = false features = ["vendored-libgit2"] - -[dependencies.git-trailers] -version = "0.1.0" -path = "../git-trailers" diff --git a/git-commit/src/lib.rs b/git-commit/src/lib.rs index 6557d5a..f9eaa77 100644 --- a/git-commit/src/lib.rs +++ b/git-commit/src/lib.rs @@ -17,7 +17,6 @@ use std::{ }; use git2::{ObjectType, Oid}; -use git_trailers::{self as trailers, OwnedTrailer, Trailer}; pub mod author; pub use author::Author; @@ -25,6 +24,9 @@ pub use author::Author; pub mod headers; pub use headers::{Headers, Signature}; +pub mod trailers; +pub use trailers::{OwnedTrailer, Trailer, Trailers}; + /// A git commit in its object description form, i.e. the output of /// `git cat-file` for a commit object. #[derive(Debug)] @@ -162,8 +164,6 @@ pub mod error { InvalidFormat, #[error("missing '{0}' while parsing commit")] Missing(&'static str), - #[error(transparent)] - Token(#[from] git_trailers::InvalidToken), #[error("error occurred while checking for git-trailers: {0}")] Trailers(#[source] git2::Error), #[error(transparent)] @@ -243,19 +243,14 @@ impl FromStr for Commit { } } - let (message, trailers) = message.lines().fold( - (Vec::new(), Vec::new()), - |(mut message, mut trailers), line| match trailers::parser::trailer(line, ": ") { - Ok((_, trailer)) => { - trailers.push(trailer.into()); - (message, trailers) - }, - Err(_) => { - message.push(line); - (message, trailers) - }, - }, - ); + let trailers = trailers::Trailers::parse(message).map_err(error::Parse::Trailers)?; + + let message = message + .strip_suffix(&trailers.to_string(": ")) + .unwrap_or(message) + .to_string(); + + let trailers = trailers.iter().map(OwnedTrailer::from).collect(); Ok(Self { tree, @@ -263,7 +258,7 @@ impl FromStr for Commit { author: author.ok_or(error::Parse::Missing("author"))?, committer: committer.ok_or(error::Parse::Missing("committer"))?, headers, - message: message.join("\n"), + message, trailers, }) } diff --git a/git-commit/src/trailers.rs b/git-commit/src/trailers.rs new file mode 100644 index 0000000..7265d81 --- /dev/null +++ b/git-commit/src/trailers.rs @@ -0,0 +1,214 @@ +use std::{borrow::Cow, fmt, fmt::Write, ops::Deref, str::FromStr}; + +use git2::{MessageTrailersStrs, MessageTrailersStrsIterator}; + +/// A Git commit's set of trailers that are left in the commit's +/// message. +/// +/// Trailers are key/value pairs in the last paragraph of a message, +/// not including any patches or conflicts that may be present. +/// +/// # Usage +/// +/// To construct `Trailers`, you can use [`Trailers::parse`] or its +/// `FromStr` implementation. +/// +/// To iterate over the trailers, you can use [`Trailers::iter`]. +/// +/// To render the trailers to a `String`, you can use +/// [`Trailers::to_string`] or its `Display` implementation (note that +/// it will default to using `": "` as the separator. +/// +/// # Examples +/// +/// ```text +/// Add new functionality +/// +/// Making code better with new functionality. +/// +/// X-Signed-Off-By: Alex Sellier +/// X-Co-Authored-By: Fintan Halpenny +/// ``` +/// +/// The trailers in the above example are: +/// +/// ```text +/// X-Signed-Off-By: Alex Sellier +/// X-Co-Authored-By: Fintan Halpenny +/// ``` +pub struct Trailers { + inner: MessageTrailersStrs, +} + +impl Trailers { + pub fn parse(message: &str) -> Result { + Ok(Self { + inner: git2::message_trailers_strs(message)?, + }) + } + + pub fn iter(&self) -> Iter<'_> { + Iter { + inner: self.inner.iter(), + } + } + + pub fn to_string<'a, S>(&self, sep: S) -> String + where + S: Separator<'a>, + { + let mut buf = String::new(); + for (i, trailer) in self.iter().enumerate() { + if i > 0 { + writeln!(buf).ok(); + } + + write!(buf, "{}", trailer.display(sep.sep_for(&trailer.token))).ok(); + } + writeln!(buf).ok(); + buf + } +} + +impl fmt::Display for Trailers { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.to_string(": ")) + } +} + +pub trait Separator<'a> { + fn sep_for(&self, token: &Token) -> &'a str; +} + +impl<'a> Separator<'a> for &'a str { + fn sep_for(&self, _: &Token) -> &'a str { + self + } +} + +impl<'a, F> Separator<'a> for F +where + F: Fn(&Token) -> &'a str, +{ + fn sep_for(&self, token: &Token) -> &'a str { + self(token) + } +} + +impl FromStr for Trailers { + type Err = git2::Error; + + fn from_str(s: &str) -> Result { + Self::parse(s) + } +} + +pub struct Iter<'a> { + inner: MessageTrailersStrsIterator<'a>, +} + +impl<'a> Iterator for Iter<'a> { + type Item = Trailer<'a>; + + fn next(&mut self) -> Option { + let (token, value) = self.inner.next()?; + Some(Trailer { + token: Token(token), + value: Cow::Borrowed(value), + }) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Token<'a>(&'a str); + +impl Deref for Token<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.0 + } +} + +pub struct Display<'a> { + trailer: &'a Trailer<'a>, + separator: &'a str, +} + +impl<'a> fmt::Display for Display<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}{}{}", + self.trailer.token.deref(), + self.separator, + self.trailer.value, + ) + } +} + +/// A trailer is a key/value pair found in the last paragraph of a Git +/// commit message, not including any patches or conflicts that may be +/// present. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Trailer<'a> { + pub token: Token<'a>, + pub value: Cow<'a, str>, +} + +impl<'a> Trailer<'a> { + pub fn display(&'a self, separator: &'a str) -> Display<'a> { + Display { + trailer: self, + separator, + } + } + + pub fn to_owned(&self) -> OwnedTrailer { + OwnedTrailer::from(self) + } +} + +/// A version of the [`Trailer`] which owns its token and +/// value. Useful for when you need to carry trailers around in a long +/// lived data structure. +#[derive(Debug)] +pub struct OwnedTrailer { + pub token: OwnedToken, + pub value: String, +} + +#[derive(Debug)] +pub struct OwnedToken(String); + +impl Deref for OwnedToken { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a> From<&Trailer<'a>> for OwnedTrailer { + fn from(t: &Trailer<'a>) -> Self { + OwnedTrailer { + token: OwnedToken(t.token.0.to_string()), + value: t.value.to_string(), + } + } +} + +impl<'a> From> for OwnedTrailer { + fn from(t: Trailer<'a>) -> Self { + (&t).into() + } +} + +impl<'a> From<&'a OwnedTrailer> for Trailer<'a> { + fn from(t: &'a OwnedTrailer) -> Self { + Trailer { + token: Token(t.token.0.as_str()), + value: Cow::from(&t.value), + } + } +} diff --git a/git-commit/t/Cargo.toml b/git-commit/t/Cargo.toml index 1d8a1df..c7dfd9c 100644 --- a/git-commit/t/Cargo.toml +++ b/git-commit/t/Cargo.toml @@ -14,7 +14,7 @@ doc = false [features] test = [] -[dependencies.git-commit] +[dev-dependencies.git-commit] path = ".." [dev-dependencies.git2]