use libc; 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}; /// A structure to represent a git [commit][1] /// /// [1]: http://git-scm.com/book/en/Git-Internals-Git-Objects pub struct Commit<'repo> { raw: *mut raw::git_commit, _marker: marker::PhantomData>, } /// An iterator over the parent commits of a commit. /// /// Aborts iteration when a commit cannot be found pub struct Parents<'commit, 'repo> { range: Range, commit: &'commit Commit<'repo>, } /// An iterator over the parent commits' ids of a commit. /// /// Aborts iteration when a commit cannot be found pub struct ParentIds<'commit> { range: Range, commit: &'commit Commit<'commit>, } impl<'repo> Commit<'repo> { /// Get the id (SHA1) of a repository commit pub fn id(&self) -> Oid { unsafe { Binding::from_raw(raw::git_commit_id(&*self.raw)) } } /// Get the id of the tree pointed to by this commit. /// /// No attempts are made to fetch an object from the ODB. pub fn tree_id(&self) -> Oid { unsafe { Binding::from_raw(raw::git_commit_tree_id(&*self.raw)) } } /// Get the tree pointed to by a commit. pub fn tree(&self) -> Result, Error> { let mut ret = ptr::null_mut(); unsafe { try_call!(raw::git_commit_tree(&mut ret, &*self.raw)); Ok(Binding::from_raw(ret)) } } /// Get access to the underlying raw pointer. pub fn raw(&self) -> *mut raw::git_commit { self.raw } /// Get the full message of a commit. /// /// The returned message will be slightly prettified by removing any /// potential leading newlines. /// /// `None` will be returned if the message is not valid utf-8 pub fn message(&self) -> Option<&str> { str::from_utf8(self.message_bytes()).ok() } /// Get the full message of a commit as a byte slice. /// /// The returned message will be slightly prettified by removing any /// potential leading newlines. pub fn message_bytes(&self) -> &[u8] { unsafe { crate::opt_bytes(self, raw::git_commit_message(&*self.raw)).unwrap() } } /// Get the encoding for the message of a commit, as a string representing a /// standard encoding name. /// /// `None` will be returned if the encoding is not known pub fn message_encoding(&self) -> Option<&str> { let bytes = unsafe { crate::opt_bytes(self, raw::git_commit_message_encoding(&*self.raw)) }; bytes.and_then(|b| str::from_utf8(b).ok()) } /// Get the full raw message of a commit. /// /// `None` will be returned if the message is not valid utf-8 pub fn message_raw(&self) -> Option<&str> { str::from_utf8(self.message_raw_bytes()).ok() } /// Get the full raw message of a commit. pub fn message_raw_bytes(&self) -> &[u8] { unsafe { crate::opt_bytes(self, raw::git_commit_message_raw(&*self.raw)).unwrap() } } /// Get the full raw text of the commit header. /// /// `None` will be returned if the message is not valid utf-8 pub fn raw_header(&self) -> Option<&str> { str::from_utf8(self.raw_header_bytes()).ok() } /// Get an arbitrary header field. pub fn header_field_bytes(&self, field: T) -> Result { 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) } /// Get the full raw text of the commit header. pub fn raw_header_bytes(&self) -> &[u8] { unsafe { crate::opt_bytes(self, raw::git_commit_raw_header(&*self.raw)).unwrap() } } /// Get the short "summary" of the git commit message. /// /// The returned message is the summary of the commit, comprising the first /// paragraph of the message with whitespace trimmed and squashed. /// /// `None` may be returned if an error occurs or if the summary is not valid /// utf-8. pub fn summary(&self) -> Option<&str> { self.summary_bytes().and_then(|s| str::from_utf8(s).ok()) } /// Get the short "summary" of the git commit message. /// /// The returned message is the summary of the commit, comprising the first /// paragraph of the message with whitespace trimmed and squashed. /// /// `None` may be returned if an error occurs pub fn summary_bytes(&self) -> Option<&[u8]> { unsafe { crate::opt_bytes(self, raw::git_commit_summary(self.raw)) } } /// Get the long "body" of the git commit message. /// /// The returned message is the body of the commit, comprising everything /// but the first paragraph of the message. Leading and trailing whitespaces /// are trimmed. /// /// `None` may be returned if an error occurs or if the summary is not valid /// utf-8. pub fn body(&self) -> Option<&str> { self.body_bytes().and_then(|s| str::from_utf8(s).ok()) } /// Get the long "body" of the git commit message. /// /// The returned message is the body of the commit, comprising everything /// but the first paragraph of the message. Leading and trailing whitespaces /// are trimmed. /// /// `None` may be returned if an error occurs. pub fn body_bytes(&self) -> Option<&[u8]> { unsafe { crate::opt_bytes(self, raw::git_commit_body(self.raw)) } } /// Get the commit time (i.e. committer time) of a commit. /// /// The first element of the tuple is the time, in seconds, since the epoch. /// The second element is the offset, in minutes, of the time zone of the /// committer's preferred time zone. 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, ) } } /// Creates a new iterator over the parents of this commit. pub fn parents<'a>(&'a self) -> Parents<'a, 'repo> { Parents { range: 0..self.parent_count(), commit: self, } } /// Creates a new iterator over the parents of this commit. pub fn parent_ids(&self) -> ParentIds<'_> { ParentIds { range: 0..self.parent_count(), commit: self, } } /// Get the author of this commit. pub fn author(&self) -> Signature<'_> { unsafe { let ptr = raw::git_commit_author(&*self.raw); signature::from_raw_const(self, ptr) } } /// Get the author of this commit, using the mailmap to map names and email /// addresses to canonical real names and email addresses. pub fn author_with_mailmap(&self, mailmap: &Mailmap) -> Result, 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)) } } /// Get the committer of this commit. pub fn committer(&self) -> Signature<'_> { unsafe { let ptr = raw::git_commit_committer(&*self.raw); signature::from_raw_const(self, ptr) } } /// Get the committer of this commit, using the mailmap to map names and email /// addresses to canonical real names and email addresses. pub fn committer_with_mailmap(&self, mailmap: &Mailmap) -> Result, 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)) } } /// Amend this existing commit with all non-`None` values /// /// This creates a new commit that is exactly the same as the old commit, /// except that any non-`None` values will be updated. The new commit has /// the same parents as the old commit. /// /// For information about `update_ref`, see [`Repository::commit`]. /// /// [`Repository::commit`]: struct.Repository.html#method.commit 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 { let mut raw = raw::git_oid { id: [0; raw::GIT_OID_RAWSZ], }; 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 _)) } } /// Get the number of parents of this commit. /// /// Use the `parents` iterator to return an iterator over all parents. pub fn parent_count(&self) -> usize { unsafe { raw::git_commit_parentcount(&*self.raw) as usize } } /// Get the specified parent of the commit. /// /// Use the `parents` iterator to return an iterator over all parents. pub fn parent(&self, i: usize) -> Result, 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)) } } /// Get the specified parent id of the commit. /// /// This is different from `parent`, which will attempt to load the /// parent commit from the ODB. /// /// Use the `parent_ids` iterator to return an iterator over all parents. pub fn parent_id(&self, i: usize) -> Result { 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)) } } } /// Casts this Commit to be usable as an `Object` pub fn as_object(&self) -> &Object<'repo> { unsafe { &*(self as *const _ as *const Object<'repo>) } } /// Consumes Commit to be returned as an `Object` pub fn into_object(self) -> Object<'repo> { assert_eq!(mem::size_of_val(&self), mem::size_of::>()); 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 Some(summary) = self.summary() { ds.field("summary", &summary); } ds.finish() } } /// Aborts iteration when a commit cannot be found impl<'repo, 'commit> Iterator for Parents<'commit, 'repo> { type Item = Commit<'repo>; fn next(&mut self) -> Option> { self.range.next().and_then(|i| self.commit.parent(i).ok()) } fn size_hint(&self) -> (usize, Option) { self.range.size_hint() } } /// Aborts iteration when a commit cannot be found impl<'repo, 'commit> DoubleEndedIterator for Parents<'commit, 'repo> { fn next_back(&mut self) -> Option> { self.range .next_back() .and_then(|i| self.commit.parent(i).ok()) } } impl<'repo, 'commit> ExactSizeIterator for Parents<'commit, 'repo> {} /// Aborts iteration when a commit cannot be found impl<'commit> Iterator for ParentIds<'commit> { type Item = Oid; fn next(&mut self) -> Option { self.range .next() .and_then(|i| self.commit.parent_id(i).ok()) } fn size_hint(&self) -> (usize, Option) { self.range.size_hint() } } /// Aborts iteration when a commit cannot be found impl<'commit> DoubleEndedIterator for ParentIds<'commit> { fn next_back(&mut self) -> Option { self.range .next_back() .and_then(|i| self.commit.parent_id(i).ok()) } } 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(), Some("initial\n\nbody")); assert_eq!(commit.body(), Some("body")); assert_eq!(commit.id(), target); commit.message_raw().unwrap(); commit.raw_header().unwrap(); commit.message_encoding(); 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() ); assert_eq!(commit.author().name(), Some("name")); assert_eq!(commit.author().email(), Some("email")); assert_eq!(commit.committer().name(), Some("name")); assert_eq!(commit.committer().email(), Some("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(), Some("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(); } }