#[cfg(test)]
mod test;
use std::borrow::Cow;
use crate::author::Author;
use super::{
headers::Headers,
trailers::{OwnedTrailer, Token, Trailer},
CommitData,
};
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
#[error("the provided commit data contained invalid UTF-8")]
Utf8(#[source] std::str::Utf8Error),
#[error("the commit header is missing the 'tree' entry")]
MissingTree,
#[error("failed to parse 'tree' value: {0}")]
InvalidTree(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("invalid format: {reason}")]
InvalidFormat { reason: &'static str },
#[error("failed to parse 'parent' value: {0}")]
InvalidParent(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("invalid header")]
InvalidHeader,
#[error("failed to parse 'author' value: {0}")]
InvalidAuthor(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("the commit header is missing the 'author' entry")]
MissingAuthor,
#[error("failed to parse 'committer' value: {0}")]
InvalidCommitter(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("the commit header is missing the 'committer' entry")]
MissingCommitter,
}
pub(super) fn parse<Tree: std::str::FromStr, Parent: std::str::FromStr>(
commit: &str,
) -> Result<CommitData<Tree, Parent>, ParseError>
where
Tree::Err: std::error::Error + Send + Sync + 'static,
Parent::Err: std::error::Error + Send + Sync + 'static,
{
let (header, body) = commit.split_once("\n\n").ok_or(ParseError::InvalidFormat {
reason: "commit headers and body must be separated by a blank line",
})?;
let (tree, parents, author, committer, headers) =
parse_headers::<Tree, Parent, Author>(header)?;
let (message, trailers) = parse_body(body);
Ok(CommitData {
tree,
parents,
author,
committer,
headers,
message,
trailers,
})
}
fn parse_headers<Tree: std::str::FromStr, Parent: std::str::FromStr, Signature: std::str::FromStr>(
header: &str,
) -> Result<(Tree, Vec<Parent>, Signature, Signature, Headers), ParseError>
where
Tree::Err: std::error::Error + Send + Sync + 'static,
Parent::Err: std::error::Error + Send + Sync + 'static,
Signature::Err: std::error::Error + Send + Sync + 'static,
{
let mut lines = header.lines();
let tree = lines
.next()
.ok_or(ParseError::MissingTree)?
.strip_prefix("tree ")
.map(Tree::from_str)
.transpose()
.map_err(|err| ParseError::InvalidTree(Box::new(err)))?
.ok_or(ParseError::MissingTree)?;
let mut parents = Vec::new();
let mut author: Option<Signature> = None;
let mut committer: Option<Signature> = None;
let mut headers = Headers::new();
for line in lines {
if let Some(rest) = line.strip_prefix(' ') {
let value: &mut String =
headers
.0
.last_mut()
.map(|(_, v)| v)
.ok_or(ParseError::InvalidFormat {
reason: "failed to parse extra header",
})?;
value.push('\n');
value.push_str(rest);
continue;
}
if let Some((name, value)) = line.split_once(' ') {
match name {
"parent" => parents.push(
value
.parse::<Parent>()
.map_err(|err| ParseError::InvalidParent(Box::new(err)))?,
),
"author" => {
author = Some(
value
.parse::<Signature>()
.map_err(|err| ParseError::InvalidAuthor(Box::new(err)))?,
)
}
"committer" => {
committer = Some(
value
.parse::<Signature>()
.map_err(|err| ParseError::InvalidCommitter(Box::new(err)))?,
)
}
_ => headers.push(name, value),
}
continue;
}
}
Ok((
tree,
parents,
author.ok_or(ParseError::MissingAuthor)?,
committer.ok_or(ParseError::MissingCommitter)?,
headers,
))
}
fn parse_body(body: &str) -> (String, Vec<OwnedTrailer>) {
let body = body.trim_end_matches('\n');
if let Some(split) = body.rfind("\n\n") {
let candidate = &body[split + 2..];
if !candidate.trim().is_empty() {
if let Some(trailers) = try_parse_trailers(candidate) {
return (body[..split].to_string(), trailers);
}
}
}
(body.to_string(), Vec::new())
}
fn try_parse_trailers(s: &str) -> Option<Vec<OwnedTrailer>> {
s.lines()
.filter(|l| !l.is_empty())
.map(|line| {
let (token_str, value) = line.split_once(": ")?;
let token = Token::try_from(token_str).ok()?;
Some(
Trailer {
token,
value: Cow::Borrowed(value),
}
.to_owned(),
)
})
.collect()
}