use std::borrow::Cow;
use nom::{
branch::alt,
bytes::{complete::is_not, complete::tag},
combinator::{all_consuming, opt},
multi::many0,
IResult,
};
use smallvec::SmallVec;
use crate::{
immutable::{object::decode, parse, parse::NL, Signature},
BStr, ByteSlice,
};
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
pub struct Commit<'a> {
#[cfg_attr(feature = "serde1", serde(borrow))]
pub tree: &'a BStr,
pub parents: SmallVec<[&'a BStr; 2]>,
pub author: Signature<'a>,
pub committer: Signature<'a>,
pub encoding: Option<&'a BStr>,
pub message: &'a BStr,
pub extra_headers: Vec<(&'a BStr, Cow<'a, BStr>)>,
}
impl<'a> Commit<'a> {
pub fn from_bytes(data: &'a [u8]) -> Result<Commit<'a>, decode::Error> {
parse(data).map(|(_, t)| t).map_err(decode::Error::from)
}
pub fn tree(&self) -> git_hash::ObjectId {
git_hash::ObjectId::from_hex(self.tree).expect("prior validation of tree hash during parsing")
}
pub fn parents(&self) -> impl Iterator<Item = git_hash::ObjectId> + '_ {
self.parents
.iter()
.map(|hex_hash| git_hash::ObjectId::from_hex(hex_hash).expect("prior validation of hashes during parsing"))
}
pub fn extra_headers(&self) -> crate::commit::ExtraHeaders<impl Iterator<Item = (&BStr, &BStr)>> {
crate::commit::ExtraHeaders::new(self.extra_headers.iter().map(|(k, v)| (*k, v.as_ref())))
}
}
fn parse_message(i: &[u8]) -> IResult<&[u8], &BStr, decode::Error> {
if i.is_empty() {
return Err(nom::Err::Error(decode::Error::NomDetail(
i.into(),
"commit message is missing",
)));
}
let (i, _) = tag(NL)(i).map_err(decode::Error::context("a newline separates headers from the message"))?;
debug_assert!(!i.is_empty());
Ok((&[], &i.as_bstr()))
}
fn parse(i: &[u8]) -> IResult<&[u8], Commit<'_>, decode::Error> {
let (i, tree) = parse::header_field(i, b"tree", parse::hex_sha1)
.map_err(decode::Error::context("tree <40 lowercase hex char>"))?;
let (i, parents) = many0(|i| parse::header_field(i, b"parent", parse::hex_sha1))(i)
.map_err(decode::Error::context("zero or more 'parent <40 lowercase hex char>'"))?;
let (i, author) =
parse::header_field(i, b"author", parse::signature).map_err(decode::Error::context("author <signature>"))?;
let (i, committer) = parse::header_field(i, b"committer", parse::signature)
.map_err(decode::Error::context("committer <signature>"))?;
let (i, encoding) = opt(|i| parse::header_field(i, b"encoding", is_not(NL)))(i)
.map_err(decode::Error::context("encoding <encoding>"))?;
let (i, extra_headers) = many0(alt((
|i| parse::any_header_field_multi_line(i).map(|(i, (k, o))| (i, (k.as_bstr(), Cow::Owned(o)))),
|i| parse::any_header_field(i, is_not(NL)).map(|(i, (k, o))| (i, (k.as_bstr(), Cow::Borrowed(o.as_bstr())))),
)))(i)
.map_err(decode::Error::context("<field> <single-line|multi-line>"))?;
let (i, message) = all_consuming(parse_message)(i)?;
Ok((
i,
Commit {
tree,
parents: SmallVec::from(parents),
author,
committer,
encoding: encoding.map(ByteSlice::as_bstr),
message,
extra_headers,
},
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn size_of_commit() {
assert_eq!(
std::mem::size_of::<Commit<'_>>(),
216,
"the size of an immutable commit shouldn't change unnoticed"
);
}
}
pub mod iter {
use crate::{
bstr::ByteSlice,
immutable::{commit::parse_message, object::decode, parse, parse::NL, Signature},
};
use bstr::BStr;
use git_hash::{oid, ObjectId};
use nom::{
branch::alt,
bytes::complete::is_not,
combinator::{all_consuming, opt},
};
use std::borrow::Cow;
#[derive(Copy, Clone)]
enum SignatureKind {
Author,
Committer,
}
enum State {
Tree,
Parents,
Signature { of: SignatureKind },
Encoding,
ExtraHeaders,
Message,
}
impl Default for State {
fn default() -> Self {
State::Tree
}
}
pub struct Iter<'a> {
data: &'a [u8],
state: State,
}
impl<'a> Iter<'a> {
pub fn from_bytes(data: &'a [u8]) -> Iter<'a> {
Iter {
data,
state: State::default(),
}
}
pub fn tree_id(&mut self) -> Option<ObjectId> {
self.next().and_then(Result::ok).and_then(Token::into_id)
}
}
impl<'a> Iter<'a> {
fn next_inner(i: &'a [u8], state: &mut State) -> Result<(&'a [u8], Token<'a>), decode::Error> {
use State::*;
Ok(match state {
Tree => {
let (i, tree) = parse::header_field(i, b"tree", parse::hex_sha1)
.map_err(decode::Error::context("tree <40 lowercase hex char>"))?;
*state = State::Parents;
(
i,
Token::Tree {
id: ObjectId::from_hex(tree).expect("parsing validation"),
},
)
}
Parents => {
let (i, parent) = opt(|i| parse::header_field(i, b"parent", parse::hex_sha1))(i)
.map_err(decode::Error::context("commit <40 lowercase hex char>"))?;
match parent {
Some(parent) => (
i,
Token::Parent {
id: ObjectId::from_hex(parent).expect("parsing validation"),
},
),
None => {
*state = State::Signature {
of: SignatureKind::Author,
};
return Self::next_inner(i, state);
}
}
}
Signature { ref mut of } => {
let who = *of;
let (field_name, err_msg) = match of {
SignatureKind::Author => {
*of = SignatureKind::Committer;
(&b"author"[..], "author <signature>")
}
SignatureKind::Committer => {
*state = State::Encoding;
(&b"committer"[..], "committer <signature>")
}
};
let (i, signature) = parse::header_field(i, field_name, parse::signature)
.map_err(decode::Error::context(err_msg))?;
(
i,
match who {
SignatureKind::Author => Token::Author { signature },
SignatureKind::Committer => Token::Committer { signature },
},
)
}
Encoding => {
let (i, encoding) = opt(|i| parse::header_field(i, b"encoding", is_not(NL)))(i)
.map_err(decode::Error::context("encoding <encoding>"))?;
*state = State::ExtraHeaders;
match encoding {
Some(encoding) => (i, Token::Encoding(encoding.as_bstr())),
None => return Self::next_inner(i, state),
}
}
ExtraHeaders => {
let (i, extra_header) = opt(alt((
|i| parse::any_header_field_multi_line(i).map(|(i, (k, o))| (i, (k.as_bstr(), Cow::Owned(o)))),
|i| {
parse::any_header_field(i, is_not(NL))
.map(|(i, (k, o))| (i, (k.as_bstr(), Cow::Borrowed(o.as_bstr()))))
},
)))(i)
.map_err(decode::Error::context("<field> <single-line|multi-line>"))?;
match extra_header {
Some(extra_header) => (i, Token::ExtraHeader(extra_header)),
None => {
*state = State::Message;
return Self::next_inner(i, state);
}
}
}
Message => {
let (i, message) = all_consuming(parse_message)(i)?;
debug_assert!(
i.is_empty(),
"we should have consumed all data - otherwise iter may go forever"
);
return Ok((i, Token::Message(message)));
}
})
}
}
impl<'a> Iterator for Iter<'a> {
type Item = Result<Token<'a>, decode::Error>;
fn next(&mut self) -> Option<Self::Item> {
if self.data.is_empty() {
return None;
}
match Self::next_inner(self.data, &mut self.state) {
Ok((data, token)) => {
self.data = data;
Some(Ok(token))
}
Err(err) => {
self.data = &[];
Some(Err(err))
}
}
}
}
#[allow(missing_docs)]
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
pub enum Token<'a> {
Tree { id: ObjectId },
Parent { id: ObjectId },
Author { signature: Signature<'a> },
Committer { signature: Signature<'a> },
Encoding(&'a BStr),
ExtraHeader((&'a BStr, Cow<'a, BStr>)),
Message(&'a BStr),
}
impl<'a> Token<'a> {
pub fn id(&self) -> Option<&oid> {
match self {
Token::Tree { id } | Token::Parent { id } => Some(id.as_ref()),
_ => None,
}
}
pub fn into_id(self) -> Option<ObjectId> {
match self {
Token::Tree { id } | Token::Parent { id } => Some(id),
_ => None,
}
}
}
}