use std::iter::FusedIterator;
use std::marker;
use std::mem;
use std::ops::Range;
use std::ptr;
use std::str;
use crate::util::Binding;
use crate::{raw, signature, Buf, Error, IntoCString, Mailmap, Object, Oid, Signature, Time, Tree};
pub struct Commit<'repo> {
raw: *mut raw::git_commit,
_marker: marker::PhantomData<Object<'repo>>,
}
pub struct Parents<'commit, 'repo> {
range: Range<usize>,
commit: &'commit Commit<'repo>,
}
pub struct ParentIds<'commit> {
range: Range<usize>,
commit: &'commit Commit<'commit>,
}
impl<'repo> Commit<'repo> {
pub fn id(&self) -> Oid {
unsafe { Binding::from_raw(raw::git_commit_id(&*self.raw)) }
}
pub fn tree_id(&self) -> Oid {
unsafe { Binding::from_raw(raw::git_commit_tree_id(&*self.raw)) }
}
pub fn tree(&self) -> Result<Tree<'repo>, Error> {
let mut ret = ptr::null_mut();
unsafe {
try_call!(raw::git_commit_tree(&mut ret, &*self.raw));
Ok(Binding::from_raw(ret))
}
}
pub fn raw(&self) -> *mut raw::git_commit {
self.raw
}
pub fn message(&self) -> Result<&str, Error> {
str::from_utf8(self.message_bytes()).map_err(|e| e.into())
}
pub fn message_bytes(&self) -> &[u8] {
unsafe { crate::opt_bytes(self, raw::git_commit_message(&*self.raw)).unwrap() }
}
pub fn message_encoding(&self) -> Result<Option<&str>, Error> {
let bytes = unsafe { crate::opt_bytes(self, raw::git_commit_message_encoding(&*self.raw)) };
match bytes {
Some(b) => str::from_utf8(b).map(|s| Some(s)).map_err(|e| e.into()),
None => Ok(None),
}
}
pub fn message_raw(&self) -> Result<&str, Error> {
str::from_utf8(self.message_raw_bytes()).map_err(|e| e.into())
}
pub fn message_raw_bytes(&self) -> &[u8] {
unsafe { crate::opt_bytes(self, raw::git_commit_message_raw(&*self.raw)).unwrap() }
}
pub fn raw_header(&self) -> Result<&str, Error> {
str::from_utf8(self.raw_header_bytes()).map_err(|e| e.into())
}
pub fn header_field_bytes<T: IntoCString>(&self, field: T) -> Result<Buf, Error> {
let buf = Buf::new();
let raw_field = field.into_c_string()?;
unsafe {
try_call!(raw::git_commit_header_field(
buf.raw(),
&*self.raw,
raw_field
));
}
Ok(buf)
}
pub fn raw_header_bytes(&self) -> &[u8] {
unsafe { crate::opt_bytes(self, raw::git_commit_raw_header(&*self.raw)).unwrap() }
}
pub fn summary(&self) -> Result<Option<&str>, Error> {
match self.summary_bytes() {
Some(sb) => str::from_utf8(sb).map(|s| Some(s)).map_err(|e| e.into()),
None => Ok(None),
}
}
pub fn summary_bytes(&self) -> Option<&[u8]> {
unsafe { crate::opt_bytes(self, raw::git_commit_summary(self.raw)) }
}
pub fn body(&self) -> Result<Option<&str>, Error> {
match self.body_bytes() {
Some(sb) => str::from_utf8(sb).map(|s| Some(s)).map_err(|e| e.into()),
None => Ok(None),
}
}
pub fn body_bytes(&self) -> Option<&[u8]> {
unsafe { crate::opt_bytes(self, raw::git_commit_body(self.raw)) }
}
pub fn time(&self) -> Time {
unsafe {
Time::new(
raw::git_commit_time(&*self.raw) as i64,
raw::git_commit_time_offset(&*self.raw) as i32,
)
}
}
pub fn parents<'a>(&'a self) -> Parents<'a, 'repo> {
Parents {
range: 0..self.parent_count(),
commit: self,
}
}
pub fn parent_ids(&self) -> ParentIds<'_> {
ParentIds {
range: 0..self.parent_count(),
commit: self,
}
}
pub fn author(&self) -> Signature<'_> {
unsafe {
let ptr = raw::git_commit_author(&*self.raw);
signature::from_raw_const(self, ptr)
}
}
pub fn author_with_mailmap(&self, mailmap: &Mailmap) -> Result<Signature<'static>, Error> {
let mut ret = ptr::null_mut();
unsafe {
try_call!(raw::git_commit_author_with_mailmap(
&mut ret,
&*self.raw,
&*mailmap.raw()
));
Ok(Binding::from_raw(ret))
}
}
pub fn committer(&self) -> Signature<'_> {
unsafe {
let ptr = raw::git_commit_committer(&*self.raw);
signature::from_raw_const(self, ptr)
}
}
pub fn committer_with_mailmap(&self, mailmap: &Mailmap) -> Result<Signature<'static>, Error> {
let mut ret = ptr::null_mut();
unsafe {
try_call!(raw::git_commit_committer_with_mailmap(
&mut ret,
&*self.raw,
&*mailmap.raw()
));
Ok(Binding::from_raw(ret))
}
}
pub fn amend(
&self,
update_ref: Option<&str>,
author: Option<&Signature<'_>>,
committer: Option<&Signature<'_>>,
message_encoding: Option<&str>,
message: Option<&str>,
tree: Option<&Tree<'repo>>,
) -> Result<Oid, Error> {
let mut raw = crate::util::zeroed_raw_oid();
let update_ref = crate::opt_cstr(update_ref)?;
let encoding = crate::opt_cstr(message_encoding)?;
let message = crate::opt_cstr(message)?;
unsafe {
try_call!(raw::git_commit_amend(
&mut raw,
self.raw(),
update_ref,
author.map(|s| s.raw()),
committer.map(|s| s.raw()),
encoding,
message,
tree.map(|t| t.raw())
));
Ok(Binding::from_raw(&raw as *const _))
}
}
pub fn parent_count(&self) -> usize {
unsafe { raw::git_commit_parentcount(&*self.raw) as usize }
}
pub fn parent(&self, i: usize) -> Result<Commit<'repo>, Error> {
unsafe {
let mut raw = ptr::null_mut();
try_call!(raw::git_commit_parent(
&mut raw,
&*self.raw,
i as libc::c_uint
));
Ok(Binding::from_raw(raw))
}
}
pub fn parent_id(&self, i: usize) -> Result<Oid, Error> {
unsafe {
let id = raw::git_commit_parent_id(self.raw, i as libc::c_uint);
if id.is_null() {
Err(Error::from_str("parent index out of bounds"))
} else {
Ok(Binding::from_raw(id))
}
}
}
pub fn as_object(&self) -> &Object<'repo> {
unsafe { &*(self as *const _ as *const Object<'repo>) }
}
pub fn into_object(self) -> Object<'repo> {
assert_eq!(mem::size_of_val(&self), mem::size_of::<Object<'_>>());
unsafe { mem::transmute(self) }
}
}
impl<'repo> Binding for Commit<'repo> {
type Raw = *mut raw::git_commit;
unsafe fn from_raw(raw: *mut raw::git_commit) -> Commit<'repo> {
Commit {
raw,
_marker: marker::PhantomData,
}
}
fn raw(&self) -> *mut raw::git_commit {
self.raw
}
}
impl<'repo> std::fmt::Debug for Commit<'repo> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
let mut ds = f.debug_struct("Commit");
ds.field("id", &self.id());
if let Ok(Some(summary)) = self.summary() {
ds.field("summary", &summary);
}
ds.finish()
}
}
impl<'repo, 'commit> Iterator for Parents<'commit, 'repo> {
type Item = Commit<'repo>;
fn next(&mut self) -> Option<Commit<'repo>> {
self.range.next().and_then(|i| self.commit.parent(i).ok())
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.range.size_hint()
}
}
impl<'repo, 'commit> DoubleEndedIterator for Parents<'commit, 'repo> {
fn next_back(&mut self) -> Option<Commit<'repo>> {
self.range
.next_back()
.and_then(|i| self.commit.parent(i).ok())
}
}
impl<'repo, 'commit> FusedIterator for Parents<'commit, 'repo> {}
impl<'repo, 'commit> ExactSizeIterator for Parents<'commit, 'repo> {}
impl<'commit> Iterator for ParentIds<'commit> {
type Item = Oid;
fn next(&mut self) -> Option<Oid> {
self.range
.next()
.and_then(|i| self.commit.parent_id(i).ok())
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.range.size_hint()
}
}
impl<'commit> DoubleEndedIterator for ParentIds<'commit> {
fn next_back(&mut self) -> Option<Oid> {
self.range
.next_back()
.and_then(|i| self.commit.parent_id(i).ok())
}
}
impl<'commit> FusedIterator for ParentIds<'commit> {}
impl<'commit> ExactSizeIterator for ParentIds<'commit> {}
impl<'repo> Clone for Commit<'repo> {
fn clone(&self) -> Self {
self.as_object().clone().into_commit().ok().unwrap()
}
}
impl<'repo> Drop for Commit<'repo> {
fn drop(&mut self) {
unsafe { raw::git_commit_free(self.raw) }
}
}
#[cfg(test)]
mod tests {
#[test]
fn smoke() {
let (_td, repo) = crate::test::repo_init();
let head = repo.head().unwrap();
let target = head.target().unwrap();
let commit = repo.find_commit(target).unwrap();
assert_eq!(commit.message(), Ok("initial\n\nbody"));
assert_eq!(commit.body(), Ok(Some("body")));
assert_eq!(commit.id(), target);
commit.message_raw().unwrap();
commit.raw_header().unwrap();
commit.message_encoding().expect("Should not be invalid");
commit.summary().unwrap();
commit.body().unwrap();
commit.tree_id();
commit.tree().unwrap();
assert_eq!(commit.parents().count(), 0);
let tree_header_bytes = commit.header_field_bytes("tree").unwrap();
assert_eq!(
crate::Oid::from_str(tree_header_bytes.as_str().unwrap()).unwrap(),
commit.tree_id()
);
let tree_oid = {
let str = tree_header_bytes.as_str().unwrap();
crate::Oid::from_str_ext(str, repo.object_format()).unwrap()
};
assert_eq!(tree_oid, commit.tree_id());
assert_eq!(commit.author().name(), Ok("name"));
assert_eq!(commit.author().email(), Ok("email"));
assert_eq!(commit.committer().name(), Ok("name"));
assert_eq!(commit.committer().email(), Ok("email"));
let sig = repo.signature().unwrap();
let tree = repo.find_tree(commit.tree_id()).unwrap();
let id = repo
.commit(Some("HEAD"), &sig, &sig, "bar", &tree, &[&commit])
.unwrap();
let head = repo.find_commit(id).unwrap();
let new_head = head
.amend(Some("HEAD"), None, None, None, Some("new message"), None)
.unwrap();
let new_head = repo.find_commit(new_head).unwrap();
assert_eq!(new_head.message(), Ok("new message"));
new_head.into_object();
repo.find_object(target, None).unwrap().as_commit().unwrap();
repo.find_object(target, None)
.unwrap()
.into_commit()
.ok()
.unwrap();
}
#[test]
#[cfg(feature = "unstable-sha256")]
fn smoke_sha256() {
let (_td, repo) = crate::test::repo_init_sha256();
let head = repo.head().unwrap();
let target = head.target().unwrap();
let commit = repo.find_commit(target).unwrap();
assert_eq!(commit.id().as_bytes().len(), 32);
assert_eq!(commit.tree_id().as_bytes().len(), 32);
assert_eq!(commit.message(), Ok("initial\n\nbody"));
assert_eq!(commit.body(), Ok(Some("body")));
assert_eq!(commit.id(), target);
commit.summary().unwrap();
commit.tree().unwrap();
assert_eq!(commit.parents().count(), 0);
let tree_header_bytes = commit.header_field_bytes("tree").unwrap();
let tree_oid = {
let str = tree_header_bytes.as_str().unwrap();
let oid = crate::Oid::from_str_ext(str, repo.object_format()).unwrap();
oid
};
assert_eq!(tree_oid, commit.tree_id());
let sig = repo.signature().unwrap();
let tree = repo.find_tree(commit.tree_id()).unwrap();
let id = repo
.commit(Some("HEAD"), &sig, &sig, "bar", &tree, &[&commit])
.unwrap();
let head = repo.find_commit(id).unwrap();
assert_eq!(head.id().as_bytes().len(), 32);
assert_eq!(head.parent_count(), 1);
assert_eq!(head.parent_id(0).unwrap(), commit.id());
}
}