//! Low-level abstraction over finding refs inside a commit tree use super::tree_utils::{self as utils, PairIter}; use crate::{branch::BranchIter, files::Yield, Commit, HashId}; use git2::{ObjectType, Repository, TreeWalkMode, TreeWalkResult}; use std::sync::Arc; /// A git directory tree walker abstraction /// /// This type is meant to be used ephemerally, and internally uses the /// libgit2 `Tree` abstraction to walk directory trees lazily to /// resolve paths to [`TreeEntry`](self::TreeEntry)'s. /// /// Note: this type _may_ be removed in the future. For a more /// high-level (and stable) API, check /// [`Explorer`](crate::files::Explorer) pub struct FileTree { repo: Arc, c: HashId, } impl FileTree { /// Construct a new FileTree with a repository pub(crate) fn new(repo: Arc, c: HashId) -> Self { Self { repo, c } } /// Get a FileTree for a new commit fn checkout(&self, c: HashId) -> Self { Self::new(Arc::clone(&self.repo), c) } /// Get the history of a path with a branch iterator pub fn base_history(&self, iter: BranchIter, path: &str) -> Option> { let mut iter = iter.peekable(); let entry = self.resolve(path)?; let trgid = entry.id(); let mut commits = vec![]; // Iterate over the branch in commit pairs while let (Some(a), b) = iter.next_pair() { dbg!(&a.commit()); let ta = self.checkout(a.commit().id.clone()); let te_a = dbg!(ta.resolve(dbg!(path))); let b = match b { Some(b) => b, None if te_a.is_some() => { // If b doesn't exist, but the path exists in a, // then it is safe to assume that a introduces the // path commits.push(a.commit().clone()); break; } None => break, }; let tb = self.checkout(b.commit().id.clone()); let te_b = match ta.resolve(path) { Some(b) => b, None => continue, }; let te_a = match te_a { Some(a) => a, None => continue, }; // If the two tree nodes are not equal, add the commit to // the list. This means that the `a` commit changed // something in the path. if dbg!(te_a != te_b) { commits.push(a.commit().clone()); } } Some(commits) } /// Enumerate a non-leaf tree entry pub fn enumerate(&self, path: &str) -> Option> { let tree = utils::open_tree(&self.repo, &self.c)?; let target = utils::path_split(path); let mut entries = vec![]; tree.walk(TreeWalkMode::PreOrder, |p, e| { let path = utils::path_split(p); if path == target { entries.push(TreeEntry::new(p, &e)); TreeWalkResult::Ok } else { TreeWalkResult::Skip } }) .ok()?; Some(entries) } /// Resolve a path inside this file tree /// /// Will return `None` if there is no tree for the selected /// commit, or the file inside the tree does not exist. pub fn resolve(&self, path: &str) -> Option { let tree = utils::open_tree(&self.repo, &self.c)?; let target = utils::path_split(path); // Initialise entry to None as a fallback let mut entry = None; // Walk over tree and swallor errors (which we use to // terminace traversal to speed up indexing time) tree.walk(TreeWalkMode::PreOrder, |p, e| { if utils::path_cmp(&target, p, e.name().unwrap()) { entry = Some(TreeEntry::new(p, &e)); TreeWalkResult::Ok } else { TreeWalkResult::Skip } }) .ok()?; // Return whatever the entry is now entry } } /// An entry in a commit tree /// /// This type is lazily loaded, and can represent either a Blob or a /// Directory. You can resolve its value by calling /// [`resolve()`](Self::resolve) #[derive(Clone, Debug, PartialEq, Eq)] pub struct TreeEntry { tt: EntryType, id: HashId, path: String, name: Option, } impl TreeEntry { fn new(path: &str, entry: &git2::TreeEntry) -> Self { let tt = match entry.kind() { Some(ObjectType::Blob) => EntryType::File, Some(ObjectType::Tree) => EntryType::Dir, _ => unimplemented!(), }; let id = entry.id().into(); let path = path.into(); let name = entry.name().map(|s| s.into()); Self { tt, id, path, name } } pub fn id(&self) -> HashId { self.id.clone() } /// Get a reference to the name of this TreeEntry pub fn name(&self) -> Option<&String> { self.name.as_ref() } /// Resolve this type to a [`Yield`]() pub fn resolve(&self) -> Yield { todo!() } } /// Type of a TreeEntry #[derive(Clone, Debug, PartialEq, Eq)] pub enum EntryType { /// A file that can be loaded File, /// A directory that can be indexed Dir, } #[test] fn s_resolve() { let path = env!("CARGO_MANIFEST_DIR").to_owned() + "/test-repo"; use crate::Repository as Repo; eprintln!("Path: `{}`", path); let r = Repo::open(&path).unwrap(); let b = r.branch("master".into()).unwrap(); let h = b.head(); let t = h.tree(); t.resolve("README").unwrap(); } #[test] fn s_enumerate() { let path = env!("CARGO_MANIFEST_DIR").to_owned() + "/test-repo"; use crate::Repository as Repo; eprintln!("Path: `{}`", path); let r = Repo::open(&path).unwrap(); let b = r.branch("master".into()).unwrap(); let h = b.head(); let t = h.tree(); let entries = t.enumerate("").unwrap(); assert_eq!( entries .iter() .filter_map(|e| e.name().map(|s| s.as_str())) .collect::>(), vec!["README", "test.rs"] ); } #[test] fn s_history() { let path = env!("CARGO_MANIFEST_DIR").to_owned() + "/test-repo"; use crate::Repository as Repo; eprintln!("Path: `{}`", path); let r = Repo::open(&path).unwrap(); let b = r.branch("master".into()).unwrap(); let head = b.head(); let iter = b.get_all(); let tree = head.tree(); let history = tree.base_history(iter, "test.rs").unwrap(); dbg!(&history); assert_eq!(history.len(), 1); }