diff --git a/rust/hg-core/src/ancestors.rs b/rust/hg-core/src/ancestors.rs --- a/rust/hg-core/src/ancestors.rs +++ b/rust/hg-core/src/ancestors.rs @@ -25,6 +25,15 @@ stoprev: Revision, } +/// Lazy ancestors set, backed by AncestorsIterator +pub struct LazyAncestors { + graph: G, + containsiter: AncestorsIterator, + initrevs: Vec, + stoprev: Revision, + inclusive: bool, +} + pub struct MissingAncestors { graph: G, bases: HashSet, @@ -98,9 +107,31 @@ } Ok(false) } + + pub fn peek(&self) -> Option { + self.visit.peek().map(|&r| r) + } + + /// Tell if the iterator is about an empty set + /// + /// The result does not depend whether the iterator has been consumed + /// or not. + /// This is mostly meant for iterators backing a lazy ancestors set + pub fn is_empty(&self) -> bool { + if self.visit.len() > 0 { + return false; + } + if self.seen.len() > 1 { + return false; + } + // at this point, the seen set is at most a singleton. + // If not `self.inclusive`, it's still possible that it has only + // the null revision + self.seen.is_empty() || self.seen.contains(&NULL_REVISION) + } } -/// Main implementation. +/// Main implementation for the iterator /// /// The algorithm is the same as in `_lazyancestorsiter()` from `ancestors.py` /// with a few non crucial differences: @@ -137,6 +168,49 @@ } } +impl LazyAncestors { + pub fn new( + graph: G, + initrevs: impl IntoIterator, + stoprev: Revision, + inclusive: bool, + ) -> Result { + let v: Vec = initrevs.into_iter().collect(); + Ok(LazyAncestors { + graph: graph.clone(), + containsiter: AncestorsIterator::new( + graph, + v.iter().cloned(), + stoprev, + inclusive, + )?, + initrevs: v, + stoprev: stoprev, + inclusive: inclusive, + }) + } + + pub fn contains(&mut self, rev: Revision) -> Result { + self.containsiter.contains(rev) + } + + pub fn is_empty(&self) -> bool { + self.containsiter.is_empty() + } + + pub fn iter(&self) -> AncestorsIterator { + // the arguments being the same as for self.containsiter, we know + // for sure that AncestorsIterator constructor can't fail + AncestorsIterator::new( + self.graph.clone(), + self.initrevs.iter().cloned(), + self.stoprev, + self.inclusive, + ) + .unwrap() + } +} + impl MissingAncestors { pub fn new(graph: G, bases: impl IntoIterator) -> Self { let mut bases: HashSet = bases.into_iter().collect(); @@ -407,7 +481,40 @@ assert!(!lazy.contains(NULL_REVISION).unwrap()); } + #[test] + fn test_peek() { + let mut iter = + AncestorsIterator::new(Stub, vec![10], 0, true).unwrap(); + // peek() gives us the next value + assert_eq!(iter.peek(), Some(10)); + // but it's not been consumed + assert_eq!(iter.next(), Some(Ok(10))); + // and iteration resumes normally + assert_eq!(iter.next(), Some(Ok(5))); + + // let's drain the iterator to test peek() at the end + while iter.next().is_some() {} + assert_eq!(iter.peek(), None); + } + + #[test] + fn test_empty() { + let mut iter = + AncestorsIterator::new(Stub, vec![10], 0, true).unwrap(); + assert!(!iter.is_empty()); + while iter.next().is_some() {} + assert!(!iter.is_empty()); + + let iter = AncestorsIterator::new(Stub, vec![], 0, true).unwrap(); + assert!(iter.is_empty()); + + // case where iter.seen == {NULL_REVISION} + let iter = AncestorsIterator::new(Stub, vec![0], 0, false).unwrap(); + assert!(iter.is_empty()); + } + /// A corrupted Graph, supporting error handling tests + #[derive(Clone, Debug)] struct Corrupted; impl Graph for Corrupted { @@ -437,6 +544,39 @@ } #[test] + fn test_lazy_iter_contains() { + let mut lazy = + LazyAncestors::new(Stub, vec![11, 13], 0, false).unwrap(); + + let revs: Vec = lazy.iter().map(|r| r.unwrap()).collect(); + // compare with iterator tests on the same initial revisions + assert_eq!(revs, vec![8, 7, 4, 3, 2, 1, 0]); + + // contains() results are correct, unaffected by the fact that + // we consumed entirely an iterator out of lazy + assert_eq!(lazy.contains(2), Ok(true)); + assert_eq!(lazy.contains(9), Ok(false)); + } + + #[test] + fn test_lazy_contains_iter() { + let mut lazy = + LazyAncestors::new(Stub, vec![11, 13], 0, false).unwrap(); // reminder: [8, 7, 4, 3, 2, 1, 0] + + assert_eq!(lazy.contains(2), Ok(true)); + assert_eq!(lazy.contains(6), Ok(false)); + + // after consumption of 2 by the inner iterator, results stay + // consistent + assert_eq!(lazy.contains(2), Ok(true)); + assert_eq!(lazy.contains(5), Ok(false)); + + // iter() still gives us a fresh iterator + let revs: Vec = lazy.iter().map(|r| r.unwrap()).collect(); + assert_eq!(revs, vec![8, 7, 4, 3, 2, 1, 0]); + } + + #[test] /// Test constructor, add/get bases fn test_missing_bases() { let mut missing_ancestors = diff --git a/rust/hg-core/src/lib.rs b/rust/hg-core/src/lib.rs --- a/rust/hg-core/src/lib.rs +++ b/rust/hg-core/src/lib.rs @@ -3,7 +3,7 @@ // This software may be used and distributed according to the terms of the // GNU General Public License version 2 or any later version. mod ancestors; -pub use ancestors::{AncestorsIterator, MissingAncestors}; +pub use ancestors::{AncestorsIterator, LazyAncestors, MissingAncestors}; /// Mercurial revision numbers /// diff --git a/rust/hg-cpython/src/cindex.rs b/rust/hg-cpython/src/cindex.rs --- a/rust/hg-cpython/src/cindex.rs +++ b/rust/hg-cpython/src/cindex.rs @@ -15,7 +15,7 @@ extern crate python3_sys as python_sys; use self::python_sys::PyCapsule_Import; -use cpython::{PyErr, PyObject, PyResult, Python}; +use cpython::{PyClone, PyErr, PyObject, PyResult, Python}; use hg::{Graph, GraphError, Revision}; use libc::c_int; use std::ffi::CStr; @@ -59,7 +59,6 @@ /// the core API, that would be tied to `GILGuard` / `Python<'p>` /// in the case of the `cpython` crate bindings yet could leave room for other /// mechanisms in other contexts. - pub struct Index { index: PyObject, parents: IndexParentsFn, @@ -74,6 +73,16 @@ } } +impl Clone for Index { + fn clone(&self) -> Self { + let guard = Python::acquire_gil(); + Index { + index: self.index.clone_ref(guard.python()), + parents: self.parents.clone(), + } + } +} + impl Graph for Index { /// wrap a call to the C extern parents function fn parents(&self, rev: Revision) -> Result<[Revision; 2], GraphError> {