| use libc::{self, c_char, c_int, c_void}; |
| use std::cmp::Ordering; |
| use std::ffi::{CStr, CString}; |
| use std::marker; |
| use std::mem; |
| use std::ops::Range; |
| use std::path::Path; |
| use std::ptr; |
| use std::str; |
| |
| use crate::util::{c_cmp_to_ordering, path_to_repo_path, Binding}; |
| use crate::{panic, raw, Error, Object, ObjectType, Oid, Repository}; |
| |
| /// A structure to represent a git [tree][1] |
| /// |
| /// [1]: http://git-scm.com/book/en/Git-Internals-Git-Objects |
| pub struct Tree<'repo> { |
| raw: *mut raw::git_tree, |
| _marker: marker::PhantomData<Object<'repo>>, |
| } |
| |
| /// A structure representing an entry inside of a tree. An entry is borrowed |
| /// from a tree. |
| pub struct TreeEntry<'tree> { |
| raw: *mut raw::git_tree_entry, |
| owned: bool, |
| _marker: marker::PhantomData<&'tree raw::git_tree_entry>, |
| } |
| |
| /// An iterator over the entries in a tree. |
| pub struct TreeIter<'tree> { |
| range: Range<usize>, |
| tree: &'tree Tree<'tree>, |
| } |
| |
| /// A binary indicator of whether a tree walk should be performed in pre-order |
| /// or post-order. |
| pub enum TreeWalkMode { |
| /// Runs the traversal in pre order. |
| PreOrder = 0, |
| /// Runs the traversal in post order. |
| PostOrder = 1, |
| } |
| |
| /// Possible return codes for tree walking callback functions. |
| #[repr(i32)] |
| pub enum TreeWalkResult { |
| /// Continue with the traversal as normal. |
| Ok = 0, |
| /// Skip the current node (in pre-order mode). |
| Skip = 1, |
| /// Completely stop the traversal. |
| Abort = raw::GIT_EUSER, |
| } |
| |
| impl Into<i32> for TreeWalkResult { |
| fn into(self) -> i32 { |
| self as i32 |
| } |
| } |
| |
| impl Into<raw::git_treewalk_mode> for TreeWalkMode { |
| #[cfg(target_env = "msvc")] |
| fn into(self) -> raw::git_treewalk_mode { |
| self as i32 |
| } |
| #[cfg(not(target_env = "msvc"))] |
| fn into(self) -> raw::git_treewalk_mode { |
| self as u32 |
| } |
| } |
| |
| impl<'repo> Tree<'repo> { |
| /// Get the id (SHA1) of a repository object |
| pub fn id(&self) -> Oid { |
| unsafe { Binding::from_raw(raw::git_tree_id(&*self.raw)) } |
| } |
| |
| /// Get the number of entries listed in this tree. |
| pub fn len(&self) -> usize { |
| unsafe { raw::git_tree_entrycount(&*self.raw) as usize } |
| } |
| |
| /// Return `true` if there is not entry |
| pub fn is_empty(&self) -> bool { |
| self.len() == 0 |
| } |
| |
| /// Returns an iterator over the entries in this tree. |
| pub fn iter(&self) -> TreeIter<'_> { |
| TreeIter { |
| range: 0..self.len(), |
| tree: self, |
| } |
| } |
| |
| /// Traverse the entries in a tree and its subtrees in post or pre order. |
| /// The callback function will be run on each node of the tree that's |
| /// walked. The return code of this function will determine how the walk |
| /// continues. |
| /// |
| /// libgit requires that the callback be an integer, where 0 indicates a |
| /// successful visit, 1 skips the node, and -1 aborts the traversal completely. |
| /// You may opt to use the enum [`TreeWalkResult`](TreeWalkResult) instead. |
| /// |
| /// ```ignore |
| /// let mut ct = 0; |
| /// tree.walk(TreeWalkMode::PreOrder, |_, entry| { |
| /// assert_eq!(entry.name(), Some("foo")); |
| /// ct += 1; |
| /// TreeWalkResult::Ok |
| /// }).unwrap(); |
| /// assert_eq!(ct, 1); |
| /// ``` |
| /// |
| /// See [libgit documentation][1] for more information. |
| /// |
| /// [1]: https://libgit2.org/libgit2/#HEAD/group/tree/git_tree_walk |
| pub fn walk<C, T>(&self, mode: TreeWalkMode, mut callback: C) -> Result<(), Error> |
| where |
| C: FnMut(&str, &TreeEntry<'_>) -> T, |
| T: Into<i32>, |
| { |
| #[allow(unused)] |
| struct TreeWalkCbData<'a, T> { |
| pub callback: &'a mut TreeWalkCb<'a, T>, |
| } |
| unsafe { |
| let mut data = TreeWalkCbData { |
| callback: &mut callback, |
| }; |
| raw::git_tree_walk( |
| self.raw(), |
| mode.into(), |
| Some(treewalk_cb::<T>), |
| &mut data as *mut _ as *mut c_void, |
| ); |
| Ok(()) |
| } |
| } |
| |
| /// Lookup a tree entry by SHA value. |
| pub fn get_id(&self, id: Oid) -> Option<TreeEntry<'_>> { |
| unsafe { |
| let ptr = raw::git_tree_entry_byid(&*self.raw(), &*id.raw()); |
| if ptr.is_null() { |
| None |
| } else { |
| Some(entry_from_raw_const(ptr)) |
| } |
| } |
| } |
| |
| /// Lookup a tree entry by its position in the tree |
| pub fn get(&self, n: usize) -> Option<TreeEntry<'_>> { |
| unsafe { |
| let ptr = raw::git_tree_entry_byindex(&*self.raw(), n as libc::size_t); |
| if ptr.is_null() { |
| None |
| } else { |
| Some(entry_from_raw_const(ptr)) |
| } |
| } |
| } |
| |
| /// Lookup a tree entry by its filename |
| pub fn get_name(&self, filename: &str) -> Option<TreeEntry<'_>> { |
| let filename = CString::new(filename).unwrap(); |
| unsafe { |
| let ptr = call!(raw::git_tree_entry_byname(&*self.raw(), filename)); |
| if ptr.is_null() { |
| None |
| } else { |
| Some(entry_from_raw_const(ptr)) |
| } |
| } |
| } |
| |
| /// Retrieve a tree entry contained in a tree or in any of its subtrees, |
| /// given its relative path. |
| pub fn get_path(&self, path: &Path) -> Result<TreeEntry<'static>, Error> { |
| let path = path_to_repo_path(path)?; |
| let mut ret = ptr::null_mut(); |
| unsafe { |
| try_call!(raw::git_tree_entry_bypath(&mut ret, &*self.raw(), path)); |
| Ok(Binding::from_raw(ret)) |
| } |
| } |
| |
| /// Casts this Tree 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) } |
| } |
| } |
| |
| type TreeWalkCb<'a, T> = dyn FnMut(&str, &TreeEntry<'_>) -> T + 'a; |
| |
| extern "C" fn treewalk_cb<T: Into<i32>>( |
| root: *const c_char, |
| entry: *const raw::git_tree_entry, |
| payload: *mut c_void, |
| ) -> c_int { |
| match panic::wrap(|| unsafe { |
| let root = match CStr::from_ptr(root).to_str() { |
| Ok(value) => value, |
| _ => return -1, |
| }; |
| let entry = entry_from_raw_const(entry); |
| let payload = payload as *mut &mut TreeWalkCb<'_, T>; |
| (*payload)(root, &entry).into() |
| }) { |
| Some(value) => value, |
| None => -1, |
| } |
| } |
| |
| impl<'repo> Binding for Tree<'repo> { |
| type Raw = *mut raw::git_tree; |
| |
| unsafe fn from_raw(raw: *mut raw::git_tree) -> Tree<'repo> { |
| Tree { |
| raw, |
| _marker: marker::PhantomData, |
| } |
| } |
| fn raw(&self) -> *mut raw::git_tree { |
| self.raw |
| } |
| } |
| |
| impl<'repo> std::fmt::Debug for Tree<'repo> { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { |
| f.debug_struct("Tree").field("id", &self.id()).finish() |
| } |
| } |
| |
| impl<'repo> Clone for Tree<'repo> { |
| fn clone(&self) -> Self { |
| self.as_object().clone().into_tree().ok().unwrap() |
| } |
| } |
| |
| impl<'repo> Drop for Tree<'repo> { |
| fn drop(&mut self) { |
| unsafe { raw::git_tree_free(self.raw) } |
| } |
| } |
| |
| impl<'repo, 'iter> IntoIterator for &'iter Tree<'repo> { |
| type Item = TreeEntry<'iter>; |
| type IntoIter = TreeIter<'iter>; |
| fn into_iter(self) -> Self::IntoIter { |
| self.iter() |
| } |
| } |
| |
| /// Create a new tree entry from the raw pointer provided. |
| /// |
| /// The lifetime of the entry is tied to the tree provided and the function |
| /// is unsafe because the validity of the pointer cannot be guaranteed. |
| pub unsafe fn entry_from_raw_const<'tree>(raw: *const raw::git_tree_entry) -> TreeEntry<'tree> { |
| TreeEntry { |
| raw: raw as *mut raw::git_tree_entry, |
| owned: false, |
| _marker: marker::PhantomData, |
| } |
| } |
| |
| impl<'tree> TreeEntry<'tree> { |
| /// Get the id of the object pointed by the entry |
| pub fn id(&self) -> Oid { |
| unsafe { Binding::from_raw(raw::git_tree_entry_id(&*self.raw)) } |
| } |
| |
| /// Get the filename of a tree entry |
| /// |
| /// Returns `None` if the name is not valid utf-8 |
| pub fn name(&self) -> Option<&str> { |
| str::from_utf8(self.name_bytes()).ok() |
| } |
| |
| /// Get the filename of a tree entry |
| pub fn name_bytes(&self) -> &[u8] { |
| unsafe { crate::opt_bytes(self, raw::git_tree_entry_name(&*self.raw())).unwrap() } |
| } |
| |
| /// Convert a tree entry to the object it points to. |
| pub fn to_object<'a>(&self, repo: &'a Repository) -> Result<Object<'a>, Error> { |
| let mut ret = ptr::null_mut(); |
| unsafe { |
| try_call!(raw::git_tree_entry_to_object( |
| &mut ret, |
| repo.raw(), |
| &*self.raw() |
| )); |
| Ok(Binding::from_raw(ret)) |
| } |
| } |
| |
| /// Get the type of the object pointed by the entry |
| pub fn kind(&self) -> Option<ObjectType> { |
| ObjectType::from_raw(unsafe { raw::git_tree_entry_type(&*self.raw) }) |
| } |
| |
| /// Get the UNIX file attributes of a tree entry |
| pub fn filemode(&self) -> i32 { |
| unsafe { raw::git_tree_entry_filemode(&*self.raw) as i32 } |
| } |
| |
| /// Get the raw UNIX file attributes of a tree entry |
| pub fn filemode_raw(&self) -> i32 { |
| unsafe { raw::git_tree_entry_filemode_raw(&*self.raw) as i32 } |
| } |
| |
| /// Convert this entry of any lifetime into an owned signature with a static |
| /// lifetime. |
| /// |
| /// This will use the `Clone::clone` implementation under the hood. |
| pub fn to_owned(&self) -> TreeEntry<'static> { |
| unsafe { |
| let me = mem::transmute::<&TreeEntry<'tree>, &TreeEntry<'static>>(self); |
| me.clone() |
| } |
| } |
| } |
| |
| impl<'a> Binding for TreeEntry<'a> { |
| type Raw = *mut raw::git_tree_entry; |
| unsafe fn from_raw(raw: *mut raw::git_tree_entry) -> TreeEntry<'a> { |
| TreeEntry { |
| raw, |
| owned: true, |
| _marker: marker::PhantomData, |
| } |
| } |
| fn raw(&self) -> *mut raw::git_tree_entry { |
| self.raw |
| } |
| } |
| |
| impl<'a> Clone for TreeEntry<'a> { |
| fn clone(&self) -> TreeEntry<'a> { |
| let mut ret = ptr::null_mut(); |
| unsafe { |
| assert_eq!(raw::git_tree_entry_dup(&mut ret, &*self.raw()), 0); |
| Binding::from_raw(ret) |
| } |
| } |
| } |
| |
| impl<'a> PartialOrd for TreeEntry<'a> { |
| fn partial_cmp(&self, other: &TreeEntry<'a>) -> Option<Ordering> { |
| Some(self.cmp(other)) |
| } |
| } |
| impl<'a> Ord for TreeEntry<'a> { |
| fn cmp(&self, other: &TreeEntry<'a>) -> Ordering { |
| c_cmp_to_ordering(unsafe { raw::git_tree_entry_cmp(&*self.raw(), &*other.raw()) }) |
| } |
| } |
| |
| impl<'a> PartialEq for TreeEntry<'a> { |
| fn eq(&self, other: &TreeEntry<'a>) -> bool { |
| self.cmp(other) == Ordering::Equal |
| } |
| } |
| impl<'a> Eq for TreeEntry<'a> {} |
| |
| impl<'a> Drop for TreeEntry<'a> { |
| fn drop(&mut self) { |
| if self.owned { |
| unsafe { raw::git_tree_entry_free(self.raw) } |
| } |
| } |
| } |
| |
| impl<'tree> Iterator for TreeIter<'tree> { |
| type Item = TreeEntry<'tree>; |
| fn next(&mut self) -> Option<TreeEntry<'tree>> { |
| self.range.next().and_then(|i| self.tree.get(i)) |
| } |
| fn size_hint(&self) -> (usize, Option<usize>) { |
| self.range.size_hint() |
| } |
| } |
| impl<'tree> DoubleEndedIterator for TreeIter<'tree> { |
| fn next_back(&mut self) -> Option<TreeEntry<'tree>> { |
| self.range.next_back().and_then(|i| self.tree.get(i)) |
| } |
| } |
| impl<'tree> ExactSizeIterator for TreeIter<'tree> {} |
| |
| #[cfg(test)] |
| mod tests { |
| use super::{TreeWalkMode, TreeWalkResult}; |
| use crate::{Object, ObjectType, Repository, Tree, TreeEntry}; |
| use std::fs::File; |
| use std::io::prelude::*; |
| use std::path::Path; |
| use tempfile::TempDir; |
| |
| pub struct TestTreeIter<'a> { |
| entries: Vec<TreeEntry<'a>>, |
| repo: &'a Repository, |
| } |
| |
| impl<'a> Iterator for TestTreeIter<'a> { |
| type Item = TreeEntry<'a>; |
| |
| fn next(&mut self) -> Option<TreeEntry<'a>> { |
| if self.entries.is_empty() { |
| None |
| } else { |
| let entry = self.entries.remove(0); |
| |
| match entry.kind() { |
| Some(ObjectType::Tree) => { |
| let obj: Object<'a> = entry.to_object(self.repo).unwrap(); |
| |
| let tree: &Tree<'a> = obj.as_tree().unwrap(); |
| |
| for entry in tree.iter() { |
| self.entries.push(entry.to_owned()); |
| } |
| } |
| _ => {} |
| } |
| |
| Some(entry) |
| } |
| } |
| } |
| |
| fn tree_iter<'repo>(tree: &Tree<'repo>, repo: &'repo Repository) -> TestTreeIter<'repo> { |
| let mut initial = vec![]; |
| |
| for entry in tree.iter() { |
| initial.push(entry.to_owned()); |
| } |
| |
| TestTreeIter { |
| entries: initial, |
| repo: repo, |
| } |
| } |
| |
| #[test] |
| fn smoke_tree_iter() { |
| let (td, repo) = crate::test::repo_init(); |
| |
| setup_repo(&td, &repo); |
| |
| let head = repo.head().unwrap(); |
| let target = head.target().unwrap(); |
| let commit = repo.find_commit(target).unwrap(); |
| |
| let tree = repo.find_tree(commit.tree_id()).unwrap(); |
| assert_eq!(tree.id(), commit.tree_id()); |
| assert_eq!(tree.len(), 1); |
| |
| for entry in tree_iter(&tree, &repo) { |
| println!("iter entry {:?}", entry.name()); |
| } |
| } |
| |
| fn setup_repo(td: &TempDir, repo: &Repository) { |
| let mut index = repo.index().unwrap(); |
| File::create(&td.path().join("foo")) |
| .unwrap() |
| .write_all(b"foo") |
| .unwrap(); |
| index.add_path(Path::new("foo")).unwrap(); |
| let id = index.write_tree().unwrap(); |
| let sig = repo.signature().unwrap(); |
| let tree = repo.find_tree(id).unwrap(); |
| let parent = repo |
| .find_commit(repo.head().unwrap().target().unwrap()) |
| .unwrap(); |
| repo.commit( |
| Some("HEAD"), |
| &sig, |
| &sig, |
| "another commit", |
| &tree, |
| &[&parent], |
| ) |
| .unwrap(); |
| } |
| |
| #[test] |
| fn smoke() { |
| let (td, repo) = crate::test::repo_init(); |
| |
| setup_repo(&td, &repo); |
| |
| let head = repo.head().unwrap(); |
| let target = head.target().unwrap(); |
| let commit = repo.find_commit(target).unwrap(); |
| |
| let tree = repo.find_tree(commit.tree_id()).unwrap(); |
| assert_eq!(tree.id(), commit.tree_id()); |
| assert_eq!(tree.len(), 1); |
| { |
| let e1 = tree.get(0).unwrap(); |
| assert!(e1 == tree.get_id(e1.id()).unwrap()); |
| assert!(e1 == tree.get_name("foo").unwrap()); |
| assert!(e1 == tree.get_path(Path::new("foo")).unwrap()); |
| assert_eq!(e1.name(), Some("foo")); |
| e1.to_object(&repo).unwrap(); |
| } |
| tree.into_object(); |
| |
| repo.find_object(commit.tree_id(), None) |
| .unwrap() |
| .as_tree() |
| .unwrap(); |
| repo.find_object(commit.tree_id(), None) |
| .unwrap() |
| .into_tree() |
| .ok() |
| .unwrap(); |
| } |
| |
| #[test] |
| fn tree_walk() { |
| let (td, repo) = crate::test::repo_init(); |
| |
| setup_repo(&td, &repo); |
| |
| let head = repo.head().unwrap(); |
| let target = head.target().unwrap(); |
| let commit = repo.find_commit(target).unwrap(); |
| let tree = repo.find_tree(commit.tree_id()).unwrap(); |
| |
| let mut ct = 0; |
| tree.walk(TreeWalkMode::PreOrder, |_, entry| { |
| assert_eq!(entry.name(), Some("foo")); |
| ct += 1; |
| 0 |
| }) |
| .unwrap(); |
| assert_eq!(ct, 1); |
| |
| let mut ct = 0; |
| tree.walk(TreeWalkMode::PreOrder, |_, entry| { |
| assert_eq!(entry.name(), Some("foo")); |
| ct += 1; |
| TreeWalkResult::Ok |
| }) |
| .unwrap(); |
| assert_eq!(ct, 1); |
| } |
| } |