| 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<Object<'repo>>, |
| } |
| |
| /// An iterator over the parent commits of a commit. |
| /// |
| /// Aborts iteration when a commit cannot be found |
| pub struct Parents<'commit, 'repo> { |
| range: Range<usize>, |
| 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<usize>, |
| 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<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)) |
| } |
| } |
| |
| /// 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<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) |
| } |
| |
| /// 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 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<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)) |
| } |
| } |
| |
| /// 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<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)) |
| } |
| } |
| |
| /// 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<Oid, Error> { |
| 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<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)) |
| } |
| } |
| |
| /// 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<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)) |
| } |
| } |
| } |
| |
| /// 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::<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 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<Commit<'repo>> { |
| self.range.next().and_then(|i| self.commit.parent(i).ok()) |
| } |
| fn size_hint(&self) -> (usize, Option<usize>) { |
| 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<Commit<'repo>> { |
| 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<Oid> { |
| self.range |
| .next() |
| .and_then(|i| self.commit.parent_id(i).ok()) |
| } |
| fn size_hint(&self) -> (usize, Option<usize>) { |
| self.range.size_hint() |
| } |
| } |
| |
| /// Aborts iteration when a commit cannot be found |
| 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> 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")); |
| assert_eq!(commit.id(), target); |
| commit.message_raw().unwrap(); |
| commit.raw_header().unwrap(); |
| commit.message_encoding(); |
| commit.summary().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(); |
| } |
| } |