pub mod headers;
pub mod trailers;
use std::{
fmt::Write as _,
str::{self, FromStr},
};
use git2::{ObjectType, Oid};
use headers::{Headers, Signature};
use trailers::{OwnedTrailer, Trailer, Trailers};
use crate::author::Author;
pub type Commit = CommitData<Oid, Oid>;
impl Commit {
pub fn read(repo: &git2::Repository, oid: Oid) -> Result<Self, error::Read> {
let odb = repo.odb()?;
let object = odb.read(oid)?;
Ok(Commit::try_from(object.data())?)
}
pub fn write(&self, repo: &git2::Repository) -> Result<Oid, error::Write> {
let odb = repo.odb().map_err(error::Write::Odb)?;
self.verify_for_write(&odb)?;
Ok(odb.write(ObjectType::Commit, self.to_string().as_bytes())?)
}
fn verify_for_write(&self, odb: &git2::Odb) -> Result<(), error::Write> {
for parent in &self.parents {
verify_object(odb, parent, ObjectType::Commit)?;
}
verify_object(odb, &self.tree, ObjectType::Tree)?;
Ok(())
}
}
#[derive(Debug)]
pub struct CommitData<Tree, Parent> {
tree: Tree,
parents: Vec<Parent>,
author: Author,
committer: Author,
headers: Headers,
message: String,
trailers: Vec<OwnedTrailer>,
}
impl<Tree, Parent> CommitData<Tree, Parent> {
pub fn new<P, I, T>(
tree: Tree,
parents: P,
author: Author,
committer: Author,
headers: Headers,
message: String,
trailers: I,
) -> Self
where
P: IntoIterator<Item = Parent>,
I: IntoIterator<Item = T>,
OwnedTrailer: From<T>,
{
let trailers = trailers.into_iter().map(OwnedTrailer::from).collect();
let parents = parents.into_iter().collect();
Self {
tree,
parents,
author,
committer,
headers,
message,
trailers,
}
}
pub fn tree(&self) -> &Tree {
&self.tree
}
pub fn parents(&self) -> impl Iterator<Item = Parent> + '_
where
Parent: Clone,
{
self.parents.iter().cloned()
}
pub fn author(&self) -> &Author {
&self.author
}
pub fn committer(&self) -> &Author {
&self.committer
}
pub fn message(&self) -> &str {
&self.message
}
pub fn signatures(&self) -> impl Iterator<Item = Signature> + '_ {
self.headers.signatures()
}
pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
self.headers.iter()
}
pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + '_ {
self.headers.values(name)
}
pub fn push_header(&mut self, name: &str, value: &str) {
self.headers.push(name, value.trim());
}
pub fn trailers(&self) -> impl Iterator<Item = &OwnedTrailer> {
self.trailers.iter()
}
pub fn map_tree<U, E, F>(self, f: F) -> Result<CommitData<U, Parent>, E>
where
F: FnOnce(Tree) -> Result<U, E>,
{
Ok(CommitData {
tree: f(self.tree)?,
parents: self.parents,
author: self.author,
committer: self.committer,
headers: self.headers,
message: self.message,
trailers: self.trailers,
})
}
pub fn map_parents<U, E, F>(self, f: F) -> Result<CommitData<Tree, U>, E>
where
F: FnMut(Parent) -> Result<U, E>,
{
Ok(CommitData {
tree: self.tree,
parents: self
.parents
.into_iter()
.map(f)
.collect::<Result<Vec<_>, _>>()?,
author: self.author,
committer: self.committer,
headers: self.headers,
message: self.message,
trailers: self.trailers,
})
}
}
fn verify_object(odb: &git2::Odb, oid: &Oid, expected: ObjectType) -> Result<(), error::Write> {
use git2::{Error, ErrorClass, ErrorCode};
let (_, kind) = odb
.read_header(*oid)
.map_err(|err| error::Write::OdbRead { oid: *oid, err })?;
if kind != expected {
Err(error::Write::NotCommit {
oid: *oid,
err: Error::new(
ErrorCode::NotFound,
ErrorClass::Object,
format!("Object '{oid}' is not expected object type {expected}"),
),
})
} else {
Ok(())
}
}
pub mod error {
use std::str;
use thiserror::Error;
use crate::author;
#[derive(Debug, Error)]
pub enum Write {
#[error(transparent)]
Git(#[from] git2::Error),
#[error("the parent '{oid}' provided is not a commit object")]
NotCommit {
oid: git2::Oid,
#[source]
err: git2::Error,
},
#[error("failed to access git odb")]
Odb(#[source] git2::Error),
#[error("failed to read '{oid}' from git odb")]
OdbRead {
oid: git2::Oid,
#[source]
err: git2::Error,
},
}
#[derive(Debug, Error)]
pub enum Read {
#[error(transparent)]
Git(#[from] git2::Error),
#[error(transparent)]
Parse(#[from] Parse),
}
#[derive(Debug, Error)]
pub enum Parse {
#[error(transparent)]
Author(#[from] author::ParseError),
#[error("invalid '{header}'")]
InvalidHeader {
header: &'static str,
#[source]
err: git2::Error,
},
#[error("invalid git commit object format")]
InvalidFormat,
#[error("missing '{0}' while parsing commit")]
Missing(&'static str),
#[error("error occurred while checking for git-trailers: {0}")]
Trailers(#[source] git2::Error),
#[error(transparent)]
Utf8(#[from] str::Utf8Error),
}
}
impl TryFrom<git2::Buf> for Commit {
type Error = error::Parse;
fn try_from(value: git2::Buf) -> Result<Self, Self::Error> {
value.as_str().ok_or(error::Parse::InvalidFormat)?.parse()
}
}
impl TryFrom<&[u8]> for Commit {
type Error = error::Parse;
fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
Commit::from_str(str::from_utf8(data)?)
}
}
impl FromStr for Commit {
type Err = error::Parse;
fn from_str(buffer: &str) -> Result<Self, Self::Err> {
let (header, message) = buffer
.split_once("\n\n")
.ok_or(error::Parse::InvalidFormat)?;
let mut lines = header.lines();
let tree = match lines.next() {
Some(tree) => tree
.strip_prefix("tree ")
.map(git2::Oid::from_str)
.transpose()
.map_err(|err| error::Parse::InvalidHeader {
header: "tree",
err,
})?
.ok_or(error::Parse::Missing("tree"))?,
None => return Err(error::Parse::Missing("tree")),
};
let mut parents = Vec::new();
let mut author: Option<Author> = None;
let mut committer: Option<Author> = 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(error::Parse::InvalidFormat)?;
value.push('\n');
value.push_str(rest);
continue;
}
if let Some((name, value)) = line.split_once(' ') {
match name {
"parent" => parents.push(git2::Oid::from_str(value).map_err(|err| {
error::Parse::InvalidHeader {
header: "parent",
err,
}
})?),
"author" => author = Some(value.parse::<Author>()?),
"committer" => committer = Some(value.parse::<Author>()?),
_ => headers.push(name, value),
}
continue;
}
}
let 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,
parents,
author: author.ok_or(error::Parse::Missing("author"))?,
committer: committer.ok_or(error::Parse::Missing("committer"))?,
headers,
message,
trailers,
})
}
}
impl ToString for Commit {
fn to_string(&self) -> String {
let mut buf = String::new();
writeln!(buf, "tree {}", self.tree).ok();
for parent in self.parents() {
writeln!(buf, "parent {parent}").ok();
}
writeln!(buf, "author {}", self.author).ok();
writeln!(buf, "committer {}", self.committer).ok();
for (name, value) in self.headers.iter() {
writeln!(buf, "{name} {}", value.replace('\n', "\n ")).ok();
}
writeln!(buf).ok();
write!(buf, "{}", self.message.trim()).ok();
writeln!(buf).ok();
if !self.trailers.is_empty() {
writeln!(buf).ok();
}
for trailer in self.trailers.iter() {
writeln!(buf, "{}", Trailer::from(trailer).display(": ")).ok();
}
buf
}
}