diff --git a/Cargo.lock b/Cargo.lock index 23ad26be6b3..cae30bec939 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2678,6 +2678,7 @@ dependencies = [ name = "gix-traverse" version = "0.38.0" dependencies = [ + "bitflags 2.4.1", "gix-commitgraph 0.24.2", "gix-date 0.8.5", "gix-hash 0.14.2", diff --git a/examples/log.rs b/examples/log.rs index be892f37e15..c143f8544cc 100644 --- a/examples/log.rs +++ b/examples/log.rs @@ -8,7 +8,7 @@ use clap::Parser; use gix::{ bstr::{BString, ByteSlice}, date::time::format, - traverse::commit::Sorting, + traverse::commit::simple::Sorting, }; fn main() { diff --git a/gitoxide-core/src/hours/mod.rs b/gitoxide-core/src/hours/mod.rs index 0418ef1f01e..4080c30c4a0 100644 --- a/gitoxide-core/src/hours/mod.rs +++ b/gitoxide-core/src/hours/mod.rs @@ -175,7 +175,7 @@ where } commit_idx += 1; } - Err(gix::traverse::commit::ancestors::Error::Find { .. }) => { + Err(gix::traverse::commit::simple::Error::Find { .. }) => { is_shallow = true; break; } diff --git a/gitoxide-core/src/pack/create.rs b/gitoxide-core/src/pack/create.rs index f8141e357d6..76bacd079c3 100644 --- a/gitoxide-core/src/pack/create.rs +++ b/gitoxide-core/src/pack/create.rs @@ -130,7 +130,7 @@ where .collect::, _>>()?; let handle = repo.objects.into_shared_arc().to_cache_arc(); let iter = Box::new( - traverse::commit::Ancestors::new(tips, traverse::commit::ancestors::State::default(), handle.clone()) + traverse::commit::Simple::new(tips, handle.clone()) .map(|res| res.map_err(|err| Box::new(err) as Box<_>).map(|c| c.id)) .inspect(move |_| progress.inc()), ); @@ -361,7 +361,7 @@ pub mod input_iteration { #[derive(Debug, thiserror::Error)] pub enum Error { #[error("input objects couldn't be iterated completely")] - Iteration(#[from] traverse::commit::ancestors::Error), + Iteration(#[from] traverse::commit::simple::Error), #[error("An error occurred while reading hashes from standard input")] InputLinesIo(#[from] std::io::Error), #[error("Could not decode hex hash provided on standard input")] diff --git a/gitoxide-core/src/query/engine/update.rs b/gitoxide-core/src/query/engine/update.rs index b5c6467e0d6..2e809f0e2b3 100644 --- a/gitoxide-core/src/query/engine/update.rs +++ b/gitoxide-core/src/query/engine/update.rs @@ -429,7 +429,7 @@ pub fn update( break; } } - Err(gix::traverse::commit::ancestors::Error::Find { .. }) => { + Err(gix::traverse::commit::simple::Error::Find { .. }) => { writeln!(err, "shallow repository - commit history is truncated").ok(); break; } diff --git a/gitoxide-core/src/repository/commitgraph/list.rs b/gitoxide-core/src/repository/commitgraph/list.rs index 59e74a699cd..d9c024ed3f1 100644 --- a/gitoxide-core/src/repository/commitgraph/list.rs +++ b/gitoxide-core/src/repository/commitgraph/list.rs @@ -2,7 +2,7 @@ pub(crate) mod function { use std::{borrow::Cow, ffi::OsString}; use anyhow::{bail, Context}; - use gix::{prelude::ObjectIdExt, traverse::commit::Sorting}; + use gix::{prelude::ObjectIdExt, traverse::commit::simple::Sorting}; use crate::OutputFormat; diff --git a/gitoxide-core/src/repository/revision/list.rs b/gitoxide-core/src/repository/revision/list.rs index ab2bb2a60ed..7050bb6a667 100644 --- a/gitoxide-core/src/repository/revision/list.rs +++ b/gitoxide-core/src/repository/revision/list.rs @@ -17,7 +17,7 @@ pub const PROGRESS_RANGE: std::ops::RangeInclusive = 0..=2; pub(crate) mod function { use anyhow::{bail, Context}; - use gix::{hashtable::HashMap, traverse::commit::Sorting, Progress}; + use gix::{hashtable::HashMap, traverse::commit::simple::Sorting, Progress}; use layout::{ backends::svg::SVGWriter, core::{base::Orientation, geometry::Point, style::StyleAttr}, diff --git a/gix-config/src/file/impls.rs b/gix-config/src/file/impls.rs index 884e7ce346b..d6ac2b76a8f 100644 --- a/gix-config/src/file/impls.rs +++ b/gix-config/src/file/impls.rs @@ -42,7 +42,7 @@ impl<'a> TryFrom<&'a BStr> for File<'a> { impl From> for BString { fn from(c: File<'_>) -> Self { - c.into() + c.to_bstring() } } diff --git a/gix-config/src/parse/comment.rs b/gix-config/src/parse/comment.rs index 6d4bb15ffb5..6d7ec145b65 100644 --- a/gix-config/src/parse/comment.rs +++ b/gix-config/src/parse/comment.rs @@ -39,7 +39,7 @@ impl Display for Comment<'_> { impl From> for BString { fn from(c: Comment<'_>) -> Self { - c.into() + c.to_bstring() } } diff --git a/gix-config/src/parse/event.rs b/gix-config/src/parse/event.rs index f528e2077d6..d88ace7ce6c 100644 --- a/gix-config/src/parse/event.rs +++ b/gix-config/src/parse/event.rs @@ -72,7 +72,7 @@ impl Display for Event<'_> { impl From> for BString { fn from(event: Event<'_>) -> Self { - event.into() + event.to_bstring() } } diff --git a/gix-config/src/parse/section/header.rs b/gix-config/src/parse/section/header.rs index ad1288f3fe0..5f4e382c90a 100644 --- a/gix-config/src/parse/section/header.rs +++ b/gix-config/src/parse/section/header.rs @@ -145,7 +145,7 @@ impl Display for Header<'_> { impl From> for BString { fn from(header: Header<'_>) -> Self { - header.into() + header.to_bstring() } } diff --git a/gix-diff/tests/tree/mod.rs b/gix-diff/tests/tree/mod.rs index 7df8f64c7c9..85fcbe6170c 100644 --- a/gix-diff/tests/tree/mod.rs +++ b/gix-diff/tests/tree/mod.rs @@ -129,11 +129,10 @@ mod changes { } fn all_commits(db: &gix_odb::Handle) -> HashMap { - use gix_traverse::commit; let mut buf = Vec::new(); let head = head_of(db); - commit::Ancestors::new(Some(head), commit::ancestors::State::default(), &db) + gix_traverse::commit::Simple::new(Some(head), &db) .collect::, _>>() .expect("valid iteration") .into_iter() diff --git a/gix-pack/src/cache/delta/from_offsets.rs b/gix-pack/src/cache/delta/from_offsets.rs index ee52f9ab9dd..fc807264d77 100644 --- a/gix-pack/src/cache/delta/from_offsets.rs +++ b/gix-pack/src/cache/delta/from_offsets.rs @@ -57,12 +57,14 @@ impl Tree { })?, ); - let anticipated_num_objects = if let Some(num_objects) = data_sorted_by_offsets.size_hint().1 { - progress.init(Some(num_objects), progress::count("objects")); - num_objects - } else { - 0 - }; + let anticipated_num_objects = data_sorted_by_offsets + .size_hint() + .1 + .map(|num_objects| { + progress.init(Some(num_objects), progress::count("objects")); + num_objects + }) + .unwrap_or_default(); let mut tree = Tree::with_capacity(anticipated_num_objects)?; { diff --git a/gix-pack/tests/pack/data/output/count_and_entries.rs b/gix-pack/tests/pack/data/output/count_and_entries.rs index a98f02d387e..c20bdf52727 100644 --- a/gix-pack/tests/pack/data/output/count_and_entries.rs +++ b/gix-pack/tests/pack/data/output/count_and_entries.rs @@ -9,7 +9,6 @@ use gix_pack::data::{ output, output::{count, entry}, }; -use gix_traverse::commit; use crate::pack::{ data::output::{db, DbKind}, @@ -241,7 +240,7 @@ fn traversals() -> crate::Result { .copied() { let head = hex_to_id("dfcb5e39ac6eb30179808bbab721e8a28ce1b52e"); - let mut commits = commit::Ancestors::new(Some(head), commit::ancestors::State::default(), db.clone()) + let mut commits = gix_traverse::commit::Simple::new(Some(head), db.clone()) .map(Result::unwrap) .map(|c| c.id) .collect::>(); diff --git a/gix-traverse/Cargo.toml b/gix-traverse/Cargo.toml index 3024d8eeb15..cc505637b37 100644 --- a/gix-traverse/Cargo.toml +++ b/gix-traverse/Cargo.toml @@ -22,3 +22,4 @@ gix-revwalk = { version = "^0.13.0", path = "../gix-revwalk" } gix-commitgraph = { version = "^0.24.2", path = "../gix-commitgraph" } smallvec = "1.10.0" thiserror = "1.0.32" +bitflags = "2" diff --git a/gix-traverse/src/commit/mod.rs b/gix-traverse/src/commit/mod.rs new file mode 100644 index 00000000000..26e9287ae70 --- /dev/null +++ b/gix-traverse/src/commit/mod.rs @@ -0,0 +1,88 @@ +//! Provide multiple traversal implementations with different performance envelopes. +//! +//! Use [`Simple`] for fast walks that maintain minimal state, or [`Topo`] for a more elaborate traversal. +use gix_hash::ObjectId; +use gix_object::FindExt; +use gix_revwalk::graph::IdMap; +use gix_revwalk::PriorityQueue; +use smallvec::SmallVec; + +/// A fast iterator over the ancestors of one or more starting commits. +pub struct Simple { + objects: Find, + cache: Option, + predicate: Predicate, + state: simple::State, + parents: Parents, + sorting: simple::Sorting, +} + +/// Simple ancestors traversal, without the need to keep track of graph-state. +pub mod simple; + +/// A commit walker that walks in topographical order, like `git rev-list +/// --topo-order` or `--date-order` depending on the chosen [`topo::Sorting`]. +/// +/// Instantiate with [`topo::Builder`]. +pub struct Topo { + commit_graph: Option, + find: Find, + predicate: Predicate, + indegrees: IdMap, + states: IdMap, + explore_queue: PriorityQueue, + indegree_queue: PriorityQueue, + topo_queue: topo::iter::Queue, + parents: Parents, + min_gen: u32, + buf: Vec, +} + +pub mod topo; + +/// Specify how to handle commit parents during traversal. +#[derive(Default, Copy, Clone)] +pub enum Parents { + /// Traverse all parents, useful for traversing the entire ancestry. + #[default] + All, + /// Only traverse along the first parent, which commonly ignores all branches. + First, +} + +/// The collection of parent ids we saw as part of the iteration. +/// +/// Note that this list is truncated if [`Parents::First`] was used. +pub type ParentIds = SmallVec<[gix_hash::ObjectId; 1]>; + +/// Information about a commit that we obtained naturally as part of the iteration. +#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub struct Info { + /// The id of the commit. + pub id: gix_hash::ObjectId, + /// All parent ids we have encountered. Note that these will be at most one if [`Parents::First`] is enabled. + pub parent_ids: ParentIds, + /// The time at which the commit was created. It will only be `Some(_)` if the chosen traversal was + /// taking dates into consideration. + pub commit_time: Option, +} + +enum Either<'buf, 'cache> { + CommitRefIter(gix_object::CommitRefIter<'buf>), + CachedCommit(gix_commitgraph::file::Commit<'cache>), +} + +fn find<'cache, 'buf, Find>( + cache: Option<&'cache gix_commitgraph::Graph>, + objects: Find, + id: &gix_hash::oid, + buf: &'buf mut Vec, +) -> Result, gix_object::find::existing_iter::Error> +where + Find: gix_object::Find, +{ + match cache.and_then(|cache| cache.commit_by_id(id).map(Either::CachedCommit)) { + Some(c) => Ok(c), + None => objects.find_commit_iter(id, buf).map(Either::CommitRefIter), + } +} diff --git a/gix-traverse/src/commit.rs b/gix-traverse/src/commit/simple.rs similarity index 74% rename from gix-traverse/src/commit.rs rename to gix-traverse/src/commit/simple.rs index c1d18ebf6ff..a4a3ff391c8 100644 --- a/gix-traverse/src/commit.rs +++ b/gix-traverse/src/commit/simple.rs @@ -1,27 +1,10 @@ -use gix_object::FindExt; +use gix_date::SecondsSinceUnixEpoch; +use gix_hash::ObjectId; +use gix_hashtable::HashSet; use smallvec::SmallVec; +use std::collections::VecDeque; -/// An iterator over the ancestors one or more starting commits -pub struct Ancestors { - objects: Find, - cache: Option, - predicate: Predicate, - state: StateMut, - parents: Parents, - sorting: Sorting, -} - -/// Specify how to handle commit parents during traversal. -#[derive(Default, Copy, Clone)] -pub enum Parents { - /// Traverse all parents, useful for traversing the entire ancestry. - #[default] - All, - /// Only traverse along the first parent, which commonly ignores all branches. - First, -} - -/// Specify how to sort commits during traversal. +/// Specify how to sort commits during a [simple](super::Simple) traversal. /// /// ### Sample History /// @@ -33,7 +16,6 @@ pub enum Parents { /// \ \ /// 3----5----6----8--- /// ``` - #[derive(Default, Debug, Copy, Clone)] pub enum Sorting { /// Commits are sorted as they are mentioned in the commit graph. @@ -69,59 +51,38 @@ pub enum Sorting { }, } -/// The collection of parent ids we saw as part of the iteration. -/// -/// Note that this list is truncated if [`Parents::First`] was used. -pub type ParentIds = SmallVec<[gix_hash::ObjectId; 1]>; +/// The error is part of the item returned by the [Ancestors](super::Simple) iterator. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error(transparent)] + Find(#[from] gix_object::find::existing_iter::Error), + #[error(transparent)] + ObjectDecode(#[from] gix_object::decode::Error), +} -/// Information about a commit that we obtained naturally as part of the iteration. -#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] -pub struct Info { - /// The id of the commit. - pub id: gix_hash::ObjectId, - /// All parent ids we have encountered. Note that these will be at most one if [`Parents::First`] is enabled. - pub parent_ids: ParentIds, - /// The time at which the commit was created. It's only `Some(_)` if sorting is not [`Sorting::BreadthFirst`], as the walk - /// needs to require the commit-date. - pub commit_time: Option, +/// The state used and potentially shared by multiple graph traversals. +#[derive(Clone)] +pub(super) struct State { + next: VecDeque, + queue: gix_revwalk::PriorityQueue, + buf: Vec, + seen: HashSet, + parents_buf: Vec, + parent_ids: SmallVec<[(ObjectId, SecondsSinceUnixEpoch); 2]>, } /// #[allow(clippy::empty_docs)] -pub mod ancestors { - use std::{ - borrow::{Borrow, BorrowMut}, - collections::VecDeque, - }; - +mod init { use gix_date::SecondsSinceUnixEpoch; use gix_hash::{oid, ObjectId}; - use gix_hashtable::HashSet; use gix_object::{CommitRefIter, FindExt}; - use smallvec::SmallVec; - - use crate::commit::{collect_parents, Ancestors, Either, Info, ParentIds, Parents, Sorting}; - /// The error is part of the item returned by the [Ancestors] iterator. - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error(transparent)] - Find(#[from] gix_object::find::existing_iter::Error), - #[error(transparent)] - ObjectDecode(#[from] gix_object::decode::Error), - } - - /// The state used and potentially shared by multiple graph traversals. - #[derive(Clone)] - pub struct State { - next: VecDeque, - queue: gix_revwalk::PriorityQueue, - buf: Vec, - seen: HashSet, - parents_buf: Vec, - parent_ids: SmallVec<[(ObjectId, SecondsSinceUnixEpoch); 2]>, - } + use super::{ + super::{simple::Sorting, Either, Info, ParentIds, Parents, Simple}, + collect_parents, Error, State, + }; impl Default for State { fn default() -> Self { @@ -146,12 +107,11 @@ pub mod ancestors { } /// Builder - impl Ancestors + impl Simple where Find: gix_object::Find, - StateMut: BorrowMut, { - /// Set the sorting method, either topological or by author date + /// Set the `sorting` method. pub fn sorting(mut self, sorting: Sorting) -> Result { self.sorting = sorting; match self.sorting { @@ -160,7 +120,7 @@ pub mod ancestors { } Sorting::ByCommitTimeNewestFirst | Sorting::ByCommitTimeNewestFirstCutoffOlderThan { .. } => { let cutoff_time = self.sorting.cutoff_time(); - let state = self.state.borrow_mut(); + let state = &mut self.state; for commit_id in state.next.drain(..) { let commit_iter = self.objects.find_commit_iter(&commit_id, &mut state.buf)?; let time = commit_iter.committer()?.time.seconds; @@ -198,7 +158,7 @@ pub mod ancestors { } fn queue_to_vecdeque(&mut self) { - let state = self.state.borrow_mut(); + let state = &mut self.state; state.next.extend( std::mem::replace(&mut state.queue, gix_revwalk::PriorityQueue::new()) .into_iter_unordered() @@ -207,41 +167,35 @@ pub mod ancestors { } } - /// Initialization - impl Ancestors bool, StateMut> + /// Lifecyle + impl Simple bool> where Find: gix_object::Find, - StateMut: BorrowMut, { /// Create a new instance. /// /// * `find` - a way to lookup new object data during traversal by their `ObjectId`, writing their data into buffer and returning /// an iterator over commit tokens if the object is present and is a commit. Caching should be implemented within this function /// as needed. - /// * `state` - all state used for the traversal. If multiple traversals are performed, allocations can be minimized by reusing - /// this state. /// * `tips` /// * the starting points of the iteration, usually commits /// * each commit they lead to will only be returned once, including the tip that started it - pub fn new(tips: impl IntoIterator>, state: StateMut, find: Find) -> Self { - Self::filtered(tips, state, find, |_| true) + pub fn new(tips: impl IntoIterator>, find: Find) -> Self { + Self::filtered(tips, find, |_| true) } } - /// Initialization - impl Ancestors + /// Lifecyle + impl Simple where Find: gix_object::Find, Predicate: FnMut(&oid) -> bool, - StateMut: BorrowMut, { /// Create a new instance with commit filtering enabled. /// /// * `find` - a way to lookup new object data during traversal by their `ObjectId`, writing their data into buffer and returning /// an iterator over commit tokens if the object is present and is a commit. Caching should be implemented within this function /// as needed. - /// * `state` - all state used for the traversal. If multiple traversals are performed, allocations can be minimized by reusing - /// this state. /// * `tips` /// * the starting points of the iteration, usually commits /// * each commit they lead to will only be returned once, including the tip that started it @@ -249,13 +203,12 @@ pub mod ancestors { /// as whether its parent commits should be traversed. pub fn filtered( tips: impl IntoIterator>, - mut state: StateMut, find: Find, mut predicate: Predicate, ) -> Self { let tips = tips.into_iter(); + let mut state = State::default(); { - let state = state.borrow_mut(); state.clear(); state.next.reserve(tips.size_hint().0); for tip in tips.map(Into::into) { @@ -275,27 +228,24 @@ pub mod ancestors { } } } + /// Access - impl Ancestors - where - StateMut: Borrow, - { - /// Return an iterator for accessing more of the current commits data. + impl Simple { + /// Return an iterator for accessing data of the current commit, parsed lazily. pub fn commit_iter(&self) -> CommitRefIter<'_> { - CommitRefIter::from_bytes(&self.state.borrow().buf) + CommitRefIter::from_bytes(&self.state.buf) } - /// Return the current commits data. + /// Return the current commits' raw data, which can be parsed using [`gix_object::CommitRef::from_bytes()`]. pub fn commit_data(&self) -> &[u8] { - &self.state.borrow().buf + &self.state.buf } } - impl Iterator for Ancestors + impl Iterator for Simple where Find: gix_object::Find, Predicate: FnMut(&oid) -> bool, - StateMut: BorrowMut, { type Item = Result; @@ -325,21 +275,20 @@ pub mod ancestors { } /// Utilities - impl Ancestors + impl Simple where Find: gix_object::Find, Predicate: FnMut(&oid) -> bool, - StateMut: BorrowMut, { fn next_by_commit_date( &mut self, cutoff_older_than: Option, ) -> Option> { - let state = self.state.borrow_mut(); + let state = &mut self.state; let (commit_time, oid) = state.queue.pop()?; let mut parents: ParentIds = Default::default(); - match super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) { + match super::super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) { Ok(Either::CachedCommit(commit)) => { if !collect_parents(&mut state.parent_ids, self.cache.as_ref(), commit.iter_parents()) { // drop corrupt caches and try again with ODB @@ -396,17 +345,16 @@ pub mod ancestors { } /// Utilities - impl Ancestors + impl Simple where Find: gix_object::Find, Predicate: FnMut(&oid) -> bool, - StateMut: BorrowMut, { fn next_by_topology(&mut self) -> Option> { - let state = self.state.borrow_mut(); + let state = &mut self.state; let oid = state.next.pop_front()?; let mut parents: ParentIds = Default::default(); - match super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) { + match super::super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) { Ok(Either::CachedCommit(commit)) => { if !collect_parents(&mut state.parent_ids, self.cache.as_ref(), commit.iter_parents()) { // drop corrupt caches and try again with ODB @@ -455,11 +403,6 @@ pub mod ancestors { } } -enum Either<'buf, 'cache> { - CommitRefIter(gix_object::CommitRefIter<'buf>), - CachedCommit(gix_commitgraph::file::Commit<'cache>), -} - fn collect_parents( dest: &mut SmallVec<[(gix_hash::ObjectId, gix_date::SecondsSinceUnixEpoch); 2]>, cache: Option<&gix_commitgraph::Graph>, @@ -481,18 +424,3 @@ fn collect_parents( } true } - -fn find<'cache, 'buf, Find>( - cache: Option<&'cache gix_commitgraph::Graph>, - objects: Find, - id: &gix_hash::oid, - buf: &'buf mut Vec, -) -> Result, gix_object::find::existing_iter::Error> -where - Find: gix_object::Find, -{ - match cache.and_then(|cache| cache.commit_by_id(id).map(Either::CachedCommit)) { - Some(c) => Ok(c), - None => objects.find_commit_iter(id, buf).map(Either::CommitRefIter), - } -} diff --git a/gix-traverse/src/commit/topo/init.rs b/gix-traverse/src/commit/topo/init.rs new file mode 100644 index 00000000000..42972d2b871 --- /dev/null +++ b/gix-traverse/src/commit/topo/init.rs @@ -0,0 +1,174 @@ +use crate::commit::topo::iter::gen_and_commit_time; +use crate::commit::topo::{Error, Sorting, WalkFlags}; +use crate::commit::{find, Info, Parents, Topo}; +use gix_hash::{oid, ObjectId}; +use gix_revwalk::graph::IdMap; +use gix_revwalk::PriorityQueue; + +/// Builder for [`Topo`]. +pub struct Builder { + commit_graph: Option, + find: Find, + predicate: Predicate, + sorting: Sorting, + parents: Parents, + tips: Vec, + ends: Vec, +} + +impl Builder bool> +where + Find: gix_object::Find, +{ + /// Create a new `Builder` for a [`Topo`] that reads commits from a repository with `find`. + /// starting at the `tips` and ending at the `ends`. Like `git rev-list + /// --topo-order ^ends... tips...`. + pub fn from_iters( + find: Find, + tips: impl IntoIterator>, + ends: Option>>, + ) -> Self { + let tips = tips.into_iter().map(Into::into).collect::>(); + let ends = ends + .map(|e| e.into_iter().map(Into::into).collect::>()) + .unwrap_or_default(); + + Self { + commit_graph: Default::default(), + find, + sorting: Default::default(), + parents: Default::default(), + tips, + ends, + predicate: |_| true, + } + } + + /// Set a `predicate` to filter out revisions from the walk. Can be used to + /// implement e.g. filtering on paths or time. This does *not* exclude the + /// parent(s) of a revision that is excluded. Specify a revision as an 'end' + /// if you want that behavior. + pub fn with_predicate(self, predicate: Predicate) -> Builder + where + Predicate: FnMut(&oid) -> bool, + { + Builder { + commit_graph: self.commit_graph, + find: self.find, + sorting: self.sorting, + parents: self.parents, + tips: self.tips, + ends: self.ends, + predicate, + } + } +} + +impl Builder +where + Find: gix_object::Find, + Predicate: FnMut(&oid) -> bool, +{ + /// Set the `sorting` to use for the topological walk. + pub fn sorting(mut self, sorting: Sorting) -> Self { + self.sorting = sorting; + self + } + + /// Specify how to handle commit `parents` during traversal. + pub fn parents(mut self, parents: Parents) -> Self { + self.parents = parents; + self + } + + /// Set or unset the `commit_graph` to use for the iteration. + pub fn with_commit_graph(mut self, commit_graph: Option) -> Self { + self.commit_graph = commit_graph; + self + } + + /// Build a new [`Topo`] instance. + /// + /// Note that merely building an instance is currently expensive. + pub fn build(self) -> Result, Error> { + let mut w = Topo { + commit_graph: self.commit_graph, + find: self.find, + predicate: self.predicate, + indegrees: IdMap::default(), + states: IdMap::default(), + explore_queue: PriorityQueue::new(), + indegree_queue: PriorityQueue::new(), + topo_queue: super::iter::Queue::new(self.sorting), + parents: self.parents, + min_gen: gix_commitgraph::GENERATION_NUMBER_INFINITY, + buf: vec![], + }; + + // Initial flags for the states of the tips and ends. All of them are + // seen and added to the explore and indegree queues. The ends are by + // definition (?) uninteresting and bottom. + let tip_flags = WalkFlags::Seen | WalkFlags::Explored | WalkFlags::InDegree; + let end_flags = tip_flags | WalkFlags::Uninteresting | WalkFlags::Bottom; + + for (id, flags) in self + .tips + .iter() + .map(|id| (id, tip_flags)) + .chain(self.ends.iter().map(|id| (id, end_flags))) + { + *w.indegrees.entry(*id).or_default() = 1; + let commit = find(w.commit_graph.as_ref(), &w.find, id, &mut w.buf)?; + let (gen, time) = gen_and_commit_time(commit)?; + + if gen < w.min_gen { + w.min_gen = gen; + } + + w.states.insert(*id, flags); + w.explore_queue.insert((gen, time), *id); + w.indegree_queue.insert((gen, time), *id); + } + + // NOTE: Parents of the ends must also be marked uninteresting for some + // reason. See handle_commit() + for id in &self.ends { + let parents = w.collect_all_parents(id)?; + for (id, _) in parents { + w.states + .entry(id) + .and_modify(|s| *s |= WalkFlags::Uninteresting) + .or_insert(WalkFlags::Uninteresting | WalkFlags::Seen); + } + } + + w.compute_indegrees_to_depth(w.min_gen)?; + + // NOTE: in Git the ends are also added to the topo_queue in addition to + // the tips, but then in simplify_commit() Git is told to ignore it. For + // now the tests pass. + for id in self.tips.iter() { + let i = w.indegrees.get(id).ok_or(Error::MissingIndegreeUnexpected)?; + + if *i != 1 { + continue; + } + + let commit = find(w.commit_graph.as_ref(), &w.find, id, &mut w.buf)?; + let (_, time) = gen_and_commit_time(commit)?; + let parent_ids = w.collect_all_parents(id)?.into_iter().map(|e| e.0).collect(); + + w.topo_queue.push( + time, + Info { + id: *id, + parent_ids, + commit_time: Some(time), + }, + ); + } + + w.topo_queue.initial_sort(); + Ok(w) + } +} diff --git a/gix-traverse/src/commit/topo/iter.rs b/gix-traverse/src/commit/topo/iter.rs new file mode 100644 index 00000000000..09f38eb7e7a --- /dev/null +++ b/gix-traverse/src/commit/topo/iter.rs @@ -0,0 +1,312 @@ +use crate::commit::topo::{Error, Sorting, WalkFlags}; +use crate::commit::{find, Either, Info, Parents, Topo}; +use gix_hash::{oid, ObjectId}; +use gix_revwalk::PriorityQueue; +use smallvec::SmallVec; + +pub(in crate::commit) type GenAndCommitTime = (u32, i64); + +// Git's priority queue works as a LIFO stack if no compare function is set, +// which is the case for `--topo-order.` However, even in that case the initial +// items of the queue are sorted according to the commit time before beginning +// the walk. +#[derive(Debug)] +pub(in crate::commit) enum Queue { + Date(PriorityQueue), + Topo(Vec<(i64, Info)>), +} + +impl Queue { + pub(super) fn new(s: Sorting) -> Self { + match s { + Sorting::DateOrder => Self::Date(PriorityQueue::new()), + Sorting::TopoOrder => Self::Topo(vec![]), + } + } + + pub(super) fn push(&mut self, commit_time: i64, info: Info) { + match self { + Self::Date(q) => q.insert(commit_time, info), + Self::Topo(q) => q.push((commit_time, info)), + } + } + + fn pop(&mut self) -> Option { + match self { + Self::Date(q) => q.pop().map(|(_, info)| info), + Self::Topo(q) => q.pop().map(|(_, info)| info), + } + } + + pub(super) fn initial_sort(&mut self) { + if let Self::Topo(ref mut inner_vec) = self { + inner_vec.sort_by(|a, b| a.0.cmp(&b.0)); + } + } +} + +impl Topo +where + Find: gix_object::Find, +{ + pub(super) fn compute_indegrees_to_depth(&mut self, gen_cutoff: u32) -> Result<(), Error> { + while let Some(((gen, _), _)) = self.indegree_queue.peek() { + if *gen >= gen_cutoff { + self.indegree_walk_step()?; + } else { + break; + } + } + + Ok(()) + } + + fn indegree_walk_step(&mut self) -> Result<(), Error> { + if let Some(((gen, _), id)) = self.indegree_queue.pop() { + self.explore_to_depth(gen)?; + + let parents = self.collect_parents(&id)?; + for (id, gen_time) in parents { + self.indegrees.entry(id).and_modify(|e| *e += 1).or_insert(2); + + let state = self.states.get_mut(&id).ok_or(Error::MissingStateUnexpected)?; + if !state.contains(WalkFlags::InDegree) { + *state |= WalkFlags::InDegree; + self.indegree_queue.insert(gen_time, id); + } + } + } + Ok(()) + } + + fn explore_to_depth(&mut self, gen_cutoff: u32) -> Result<(), Error> { + while let Some(((gen, _), _)) = self.explore_queue.peek() { + if *gen >= gen_cutoff { + self.explore_walk_step()?; + } else { + break; + } + } + Ok(()) + } + + fn explore_walk_step(&mut self) -> Result<(), Error> { + if let Some((_, id)) = self.explore_queue.pop() { + let parents = self.collect_parents(&id)?; + self.process_parents(&id, &parents)?; + + for (id, gen_time) in parents { + let state = self.states.get_mut(&id).ok_or(Error::MissingStateUnexpected)?; + + if !state.contains(WalkFlags::Explored) { + *state |= WalkFlags::Explored; + self.explore_queue.insert(gen_time, id); + } + } + } + Ok(()) + } + + fn expand_topo_walk(&mut self, id: &oid) -> Result<(), Error> { + let parents = self.collect_parents(id)?; + self.process_parents(id, &parents)?; + + for (pid, (parent_gen, parent_commit_time)) in parents { + let parent_state = self.states.get(&pid).ok_or(Error::MissingStateUnexpected)?; + if parent_state.contains(WalkFlags::Uninteresting) { + continue; + } + + if parent_gen < self.min_gen { + self.min_gen = parent_gen; + self.compute_indegrees_to_depth(self.min_gen)?; + } + + let i = self.indegrees.get_mut(&pid).ok_or(Error::MissingIndegreeUnexpected)?; + *i -= 1; + if *i != 1 { + continue; + } + + let parent_ids = self.collect_all_parents(&pid)?.into_iter().map(|e| e.0).collect(); + self.topo_queue.push( + parent_commit_time, + Info { + id: pid, + parent_ids, + commit_time: Some(parent_commit_time), + }, + ); + } + + Ok(()) + } + + fn process_parents(&mut self, id: &oid, parents: &[(ObjectId, GenAndCommitTime)]) -> Result<(), Error> { + let state = self.states.get_mut(id).ok_or(Error::MissingStateUnexpected)?; + if state.contains(WalkFlags::Added) { + return Ok(()); + } + + *state |= WalkFlags::Added; + + // If the current commit is uninteresting we pass that on to ALL + // parents, otherwise we set the Seen flag. + let (pass, insert) = if state.contains(WalkFlags::Uninteresting) { + let flags = WalkFlags::Uninteresting; + for (id, _) in parents { + let grand_parents = self.collect_all_parents(id)?; + + for (id, _) in &grand_parents { + self.states + .entry(*id) + .and_modify(|s| *s |= WalkFlags::Uninteresting) + .or_insert(WalkFlags::Uninteresting | WalkFlags::Seen); + } + } + (flags, flags) + } else { + // NOTE: git sets SEEN like we do but keeps the SYMMETRIC_LEFT and + // ANCENSTRY_PATH if they are set, but they have no purpose here. + let flags = WalkFlags::empty(); + (flags, WalkFlags::Seen) + }; + + for (id, _) in parents { + self.states.entry(*id).and_modify(|s| *s |= pass).or_insert(insert); + } + Ok(()) + } + + fn collect_parents(&mut self, id: &oid) -> Result, Error> { + collect_parents( + self.commit_graph.as_ref(), + &self.find, + id, + matches!(self.parents, Parents::First), + &mut self.buf, + ) + } + + // Same as collect_parents but disregards the first_parent flag + pub(super) fn collect_all_parents( + &mut self, + id: &oid, + ) -> Result, Error> { + collect_parents(self.commit_graph.as_ref(), &self.find, id, false, &mut self.buf) + } + + fn pop_commit(&mut self) -> Option> { + let commit = self.topo_queue.pop()?; + let i = match self.indegrees.get_mut(&commit.id) { + Some(i) => i, + None => { + return Some(Err(Error::MissingIndegreeUnexpected)); + } + }; + + *i = 0; + if let Err(e) = self.expand_topo_walk(&commit.id) { + return Some(Err(e)); + }; + + Some(Ok(commit)) + } +} + +impl Iterator for Topo +where + Find: gix_object::Find, + Predicate: FnMut(&oid) -> bool, +{ + type Item = Result; + + fn next(&mut self) -> Option { + loop { + match self.pop_commit()? { + Ok(id) => { + if (self.predicate)(&id.id) { + return Some(Ok(id)); + } + } + Err(e) => return Some(Err(e)), + } + } + } +} + +fn collect_parents( + cache: Option<&gix_commitgraph::Graph>, + f: Find, + id: &oid, + first_only: bool, + buf: &mut Vec, +) -> Result, Error> +where + Find: gix_object::Find, +{ + let mut parents = SmallVec::<[(ObjectId, GenAndCommitTime); 1]>::new(); + match find(cache, &f, id, buf)? { + Either::CommitRefIter(c) => { + for token in c { + use gix_object::commit::ref_iter::Token as T; + match token { + Ok(T::Tree { .. }) => continue, + Ok(T::Parent { id }) => { + parents.push((id, (0, 0))); // Dummy numbers to be filled in + if first_only { + break; + } + } + Ok(_past_parents) => break, + Err(err) => return Err(err.into()), + } + } + // Need to check the cache again. That a commit is not in the cache + // doesn't mean a parent is not. + for (id, gen_time) in parents.iter_mut() { + let commit = find(cache, &f, id, buf)?; + *gen_time = gen_and_commit_time(commit)?; + } + } + Either::CachedCommit(c) => { + for pos in c.iter_parents() { + let parent_commit = cache + .expect("cache exists if CachedCommit was returned") + .commit_at(pos?); + parents.push(( + parent_commit.id().into(), + (parent_commit.generation(), parent_commit.committer_timestamp() as i64), + )); + if first_only { + break; + } + } + } + }; + Ok(parents) +} + +pub(super) fn gen_and_commit_time(c: Either<'_, '_>) -> Result { + match c { + Either::CommitRefIter(c) => { + let mut commit_time = 0; + for token in c { + use gix_object::commit::ref_iter::Token as T; + match token { + Ok(T::Tree { .. }) => continue, + Ok(T::Parent { .. }) => continue, + Ok(T::Author { .. }) => continue, + Ok(T::Committer { signature }) => { + commit_time = signature.time.seconds; + break; + } + Ok(_unused_token) => break, + Err(err) => return Err(err.into()), + } + } + Ok((gix_commitgraph::GENERATION_NUMBER_INFINITY, commit_time)) + } + Either::CachedCommit(c) => Ok((c.generation(), c.committer_timestamp() as i64)), + } +} diff --git a/gix-traverse/src/commit/topo/mod.rs b/gix-traverse/src/commit/topo/mod.rs new file mode 100644 index 00000000000..6ae543c106c --- /dev/null +++ b/gix-traverse/src/commit/topo/mod.rs @@ -0,0 +1,70 @@ +//! Topological commit traversal, similar to `git log --topo-order`, which keeps track of graph state. + +use bitflags::bitflags; + +/// The errors that can occur during creation and iteration. +#[derive(thiserror::Error, Debug)] +#[allow(missing_docs)] +pub enum Error { + #[error("Indegree information is missing")] + MissingIndegreeUnexpected, + #[error("Internal state (bitflags) not found")] + MissingStateUnexpected, + #[error(transparent)] + CommitGraphFile(#[from] gix_commitgraph::file::commit::Error), + #[error(transparent)] + ObjectDecode(#[from] gix_object::decode::Error), + #[error(transparent)] + Find(#[from] gix_object::find::existing_iter::Error), +} + +bitflags! { + /// Set of flags to describe the state of a particular commit while iterating. + // NOTE: The names correspond to the names of the flags in revision.h + #[repr(transparent)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub(super) struct WalkFlags: u32 { + /// Commit has been seen + const Seen = 0b000001; + /// Commit has been processed by the Explore walk + const Explored = 0b000010; + /// Commit has been processed by the Indegree walk + const InDegree = 0b000100; + /// Commit is deemed uninteresting for whatever reason + const Uninteresting = 0b001000; + /// Commit marks the end of a walk, like `foo` in `git rev-list foo..bar` + const Bottom = 0b010000; + /// Parents have been processed + const Added = 0b100000; + } +} + +/// Sorting to use for the topological walk. +/// +/// ### Sample History +/// +/// The following history will be referred to for explaining how the sort order works, with the number denoting the commit timestamp +/// (*their X-alignment doesn't matter*). +/// +/// ```text +/// ---1----2----4----7 <- second parent of 8 +/// \ \ +/// 3----5----6----8--- +/// ``` +#[derive(Clone, Copy, Debug, Default)] +pub enum Sorting { + /// Show no parents before all of its children are shown, but otherwise show + /// commits in the commit timestamp order. + #[default] + DateOrder, + /// Show no parents before all of its children are shown, and avoid + /// showing commits on multiple lines of history intermixed. + /// + /// In the *sample history* the order would be `8, 6, 5, 3, 7, 4, 2, 1` + TopoOrder, +} + +mod init; +pub use init::Builder; + +pub(super) mod iter; diff --git a/gix-traverse/src/lib.rs b/gix-traverse/src/lib.rs index 3cf6d2b3af7..49776318332 100644 --- a/gix-traverse/src/lib.rs +++ b/gix-traverse/src/lib.rs @@ -2,7 +2,6 @@ #![deny(missing_docs, rust_2018_idioms)] #![forbid(unsafe_code)] -/// Commit traversal pub mod commit; /// Tree traversal diff --git a/gix-traverse/tests/commit/mod.rs b/gix-traverse/tests/commit/mod.rs index 6d9fac3e9c8..7aa327bfa53 100644 --- a/gix-traverse/tests/commit/mod.rs +++ b/gix-traverse/tests/commit/mod.rs @@ -1,430 +1,2 @@ -mod ancestor { - use gix_hash::{oid, ObjectId}; - use gix_traverse::commit; - - use crate::hex_to_id; - - struct TraversalAssertion<'a> { - init_script: &'a str, - repo_name: &'a str, - tips: &'a [&'a str], - expected: &'a [&'a str], - mode: commit::Parents, - sorting: commit::Sorting, - } - - impl<'a> TraversalAssertion<'a> { - fn new(init_script: &'a str, tips: &'a [&'a str], expected: &'a [&'a str]) -> Self { - Self::new_at(init_script, "", tips, expected) - } - - fn new_at(init_script: &'a str, repo_name: &'a str, tips: &'a [&'a str], expected: &'a [&'a str]) -> Self { - TraversalAssertion { - init_script, - repo_name, - tips, - expected, - mode: Default::default(), - sorting: Default::default(), - } - } - - fn with_parents(&mut self, mode: commit::Parents) -> &mut Self { - self.mode = mode; - self - } - - fn with_sorting(&mut self, sorting: commit::Sorting) -> &mut Self { - self.sorting = sorting; - self - } - } - - impl TraversalAssertion<'_> { - fn setup(&self) -> crate::Result<(gix_odb::Handle, Vec, Vec)> { - let dir = gix_testtools::scripted_fixture_read_only_standalone(self.init_script)?; - let store = gix_odb::at(dir.join(self.repo_name).join(".git").join("objects"))?; - let tips: Vec<_> = self.tips.iter().copied().map(hex_to_id).collect(); - let expected: Vec = tips - .clone() - .into_iter() - .chain(self.expected.iter().map(|hex_id| hex_to_id(hex_id))) - .collect(); - Ok((store, tips, expected)) - } - - fn setup_commitgraph(&self, store: &gix_odb::Store, use_graph: bool) -> Option { - use_graph - .then(|| gix_commitgraph::at(store.path().join("info"))) - .transpose() - .expect("graph can be loaded if it exists") - } - - fn check_with_predicate(&mut self, predicate: impl FnMut(&oid) -> bool + Clone) -> crate::Result<()> { - let (store, tips, expected) = self.setup()?; - - for use_commitgraph in [false, true] { - let oids = commit::Ancestors::filtered( - tips.clone(), - commit::ancestors::State::default(), - &store, - predicate.clone(), - ) - .sorting(self.sorting)? - .parents(self.mode) - .commit_graph(self.setup_commitgraph(store.store_ref(), use_commitgraph)) - .map(|res| res.map(|info| info.id)) - .collect::, _>>()?; - - assert_eq!(oids, expected); - } - Ok(()) - } - - fn check(&self) -> crate::Result { - let (store, tips, expected) = self.setup()?; - - for use_commitgraph in [false, true] { - let oids = commit::Ancestors::new(tips.clone(), commit::ancestors::State::default(), &store) - .sorting(self.sorting)? - .parents(self.mode) - .commit_graph(self.setup_commitgraph(store.store_ref(), use_commitgraph)) - .map(|res| res.map(|info| info.id)) - .collect::, _>>()?; - assert_eq!(oids, expected); - } - Ok(()) - } - } - - mod different_date_intermixed { - use gix_traverse::commit::Sorting; - - use crate::commit::ancestor::TraversalAssertion; - - #[test] - fn head_breadth_first() -> crate::Result { - TraversalAssertion::new_at( - "make_repos.sh", - "intermixed", - &["58912d92944087dcb09dca79cdd2a937cc158bed"], /* merge */ - // This is very different from what git does as it keeps commits together, - // whereas we spread them out breadth-first. - &[ - "2dce37be587e07caef8c4a5ab60b423b13a8536a", /* c3 */ - "0f6632a5a7d81417488b86692b729e49c1b73056", /* b1c2 */ - "a9c28710e058af4e5163699960234adb9fb2abc7", /* b2c2 */ - "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ - "77fd3c6832c0cd542f7a39f3af9250c3268db979", /* b1c1 */ - "b648f955b930ca95352fae6f22cb593ee0244b27", /* b2c1 */ - "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ - ], - ) - .check() - } - - #[test] - fn head_date_order() -> crate::Result { - TraversalAssertion::new_at( - "make_repos.sh", - "intermixed", - &["58912d92944087dcb09dca79cdd2a937cc158bed"], /* merge */ - // This is exactly what git shows. - &[ - "2dce37be587e07caef8c4a5ab60b423b13a8536a", /* c3 */ - "0f6632a5a7d81417488b86692b729e49c1b73056", /* b1c2 */ - "a9c28710e058af4e5163699960234adb9fb2abc7", /* b2c2 */ - "77fd3c6832c0cd542f7a39f3af9250c3268db979", /* b1c1 */ - "b648f955b930ca95352fae6f22cb593ee0244b27", /* b2c1 */ - "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ - "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ - ], - ) - .with_sorting(Sorting::ByCommitTimeNewestFirst) - .check() - } - } - - mod different_date { - use gix_traverse::commit::Sorting; - - use crate::commit::ancestor::TraversalAssertion; - - #[test] - fn head_breadth_first() -> crate::Result { - TraversalAssertion::new_at( - "make_repos.sh", - "simple", - &["f49838d84281c3988eeadd988d97dd358c9f9dc4"], /* merge */ - // This is very different from what git does as it keeps commits together, - // whereas we spread them out breadth-first. - &[ - "0edb95c0c0d9933d88f532ec08fcd405d0eee882", /* c5 */ - "66a309480201c4157b0eae86da69f2d606aadbe7", /* b1c2 */ - "48e8dac19508f4238f06c8de2b10301ce64a641c", /* b2c2 */ - "8cb5f13b66ce52a49399a2c49f537ee2b812369c", /* c4 */ - "80947acb398362d8236fcb8bf0f8a9dac640583f", /* b1c1 */ - "cb6a6befc0a852ac74d74e0354e0f004af29cb79", /* b2c1 */ - "33aa07785dd667c0196064e3be3c51dd9b4744ef", /* c3 */ - "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ - "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ - ], - ) - .check() - } - - #[test] - fn head_date_order() -> crate::Result { - TraversalAssertion::new_at( - "make_repos.sh", - "simple", - &["f49838d84281c3988eeadd988d97dd358c9f9dc4"], /* merge */ - // This is exactly what git shows. - &[ - "0edb95c0c0d9933d88f532ec08fcd405d0eee882", /* c5 */ - "66a309480201c4157b0eae86da69f2d606aadbe7", /* b1c2 */ - "80947acb398362d8236fcb8bf0f8a9dac640583f", /* b1c1 */ - "48e8dac19508f4238f06c8de2b10301ce64a641c", /* b2c2 */ - "cb6a6befc0a852ac74d74e0354e0f004af29cb79", /* b2c1 */ - "8cb5f13b66ce52a49399a2c49f537ee2b812369c", /* c4 */ - "33aa07785dd667c0196064e3be3c51dd9b4744ef", /* c3 */ - "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ - "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ - ], - ) - .with_sorting(Sorting::ByCommitTimeNewestFirst) - .check() - } - } - - /// Same dates are somewhat special as they show how sorting-details on priority queues affects ordering - mod same_date { - use gix_traverse::commit::{Parents, Sorting}; - - use crate::{commit::ancestor::TraversalAssertion, hex_to_id}; - - #[test] - fn c4_breadth_first() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_same_date.sh", - &["9556057aee5abb06912922e9f26c46386a816822"], /* c4 */ - &[ - "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .check() - } - - #[test] - fn head_breadth_first() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_same_date.sh", - &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ - // We always take the first parent first, then the second, and so on. - // Deviation: git for some reason displays b1c2 *before* c5, but I think it's better - // to have a strict parent order. - &[ - "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ - "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ - "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ - "9152eeee2328073cf23dcf8e90c949170b711659", /* b1c1 */ - "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .check() - } - - #[test] - fn head_date_order() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_same_date.sh", - &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ - &[ - "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ - "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ - "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ - "9152eeee2328073cf23dcf8e90c949170b711659", /* b1c1 */ - "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .with_sorting(Sorting::ByCommitTimeNewestFirst) - .check() - } - - #[test] - fn head_first_parent_only_breadth_first() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_same_date.sh", - &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ - &[ - "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ - "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ - "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .with_parents(Parents::First) - .check() - } - - #[test] - fn head_c4_breadth_first() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_same_date.sh", - &[ - "01ec18a3ebf2855708ad3c9d244306bc1fae3e9b", /* m1b1 */ - "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ - ], - &[ - "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ - "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ - "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ - "9152eeee2328073cf23dcf8e90c949170b711659", /* b1c1 */ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .check() - } - - #[test] - fn filtered_commit_does_not_block_ancestors_reachable_from_another_commit() -> crate::Result { - // I don't see a use case for the predicate returning false for a commit but return true for - // at least one of its ancestors, so this test is kind of dubious. But we do want - // `Ancestors` to not eagerly blacklist all of a commit's ancestors when blacklisting that - // one commit, and this test happens to check that. - TraversalAssertion::new( - "make_traversal_repo_for_commits_same_date.sh", - &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ - &[ - "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ - "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ - "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ - "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .check_with_predicate(|id| id != hex_to_id("9152eeee2328073cf23dcf8e90c949170b711659")) - } - - #[test] - fn predicate_only_called_once_even_if_fork_point() -> crate::Result { - // The `self.seen` check should come before the `self.predicate` check, as we don't know how - // expensive calling `self.predicate` may be. - let mut seen = false; - TraversalAssertion::new( - "make_traversal_repo_for_commits_same_date.sh", - &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ - &[ - "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ - "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ - "9152eeee2328073cf23dcf8e90c949170b711659", /* b1c1 */ - ], - ) - .check_with_predicate(move |id| { - if id == hex_to_id("9556057aee5abb06912922e9f26c46386a816822") { - assert!(!seen); - seen = true; - false - } else { - true - } - }) - } - } - - /// Some dates adjusted to be a year apart, but still 'c1' and 'c2' with the same date. - mod adjusted_dates { - use gix_traverse::commit::{ancestors, Ancestors, Parents, Sorting}; - - use crate::{commit::ancestor::TraversalAssertion, hex_to_id}; - - #[test] - fn head_breadth_first() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_with_dates.sh", - &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ - // Here `git` also shows `b1c1` first, making topo-order similar to date order for some reason, - // even though c2 *is* the first parent. - &[ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac", /* b1c1 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .check() - } - - #[test] - fn head_date_order() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_with_dates.sh", - &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ - &[ - "bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac", /* b1c1 */ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .with_sorting(Sorting::ByCommitTimeNewestFirst) - .check() - } - - #[test] - fn head_date_order_with_cutoff() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_with_dates.sh", - &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ - &["bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac"], /* b1c1 */ - ) - .with_sorting(Sorting::ByCommitTimeNewestFirstCutoffOlderThan { - seconds: 978393600, // =2001-01-02 00:00:00 +0000 - }) - .check() - } - - #[test] - fn date_order_with_cutoff_is_applied_to_starting_position() -> crate::Result { - let dir = - gix_testtools::scripted_fixture_read_only_standalone("make_traversal_repo_for_commits_with_dates.sh")?; - let store = gix_odb::at(dir.join(".git").join("objects"))?; - let iter = Ancestors::new( - Some(hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7" /* c2 */)), - ancestors::State::default(), - &store, - ) - .sorting(Sorting::ByCommitTimeNewestFirstCutoffOlderThan { - seconds: 978393600, // =2001-01-02 00:00:00 +0000 - })?; - assert_eq!( - iter.count(), - 0, - "initial tips that don't pass cutoff value are not returned either" - ); - Ok(()) - } - - #[test] - fn head_date_order_first_parent_only() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_with_dates.sh", - &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ - &[ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .with_sorting(Sorting::ByCommitTimeNewestFirst) - .with_parents(Parents::First) - .check() - } - } -} +mod simple; +mod topo; diff --git a/gix-traverse/tests/commit/simple.rs b/gix-traverse/tests/commit/simple.rs new file mode 100644 index 00000000000..983941f35de --- /dev/null +++ b/gix-traverse/tests/commit/simple.rs @@ -0,0 +1,422 @@ +use gix_hash::{oid, ObjectId}; +use gix_traverse::commit; + +use crate::hex_to_id; + +struct TraversalAssertion<'a> { + init_script: &'a str, + repo_name: &'a str, + tips: &'a [&'a str], + expected: &'a [&'a str], + mode: commit::Parents, + sorting: commit::simple::Sorting, +} + +impl<'a> TraversalAssertion<'a> { + fn new(init_script: &'a str, tips: &'a [&'a str], expected: &'a [&'a str]) -> Self { + Self::new_at(init_script, "", tips, expected) + } + + fn new_at(init_script: &'a str, repo_name: &'a str, tips: &'a [&'a str], expected: &'a [&'a str]) -> Self { + TraversalAssertion { + init_script, + repo_name, + tips, + expected, + mode: Default::default(), + sorting: Default::default(), + } + } + + fn with_parents(&mut self, mode: commit::Parents) -> &mut Self { + self.mode = mode; + self + } + + fn with_sorting(&mut self, sorting: commit::simple::Sorting) -> &mut Self { + self.sorting = sorting; + self + } +} + +impl TraversalAssertion<'_> { + fn setup(&self) -> crate::Result<(gix_odb::Handle, Vec, Vec)> { + let dir = gix_testtools::scripted_fixture_read_only_standalone(self.init_script)?; + let store = gix_odb::at(dir.join(self.repo_name).join(".git").join("objects"))?; + let tips: Vec<_> = self.tips.iter().copied().map(hex_to_id).collect(); + let expected: Vec = tips + .clone() + .into_iter() + .chain(self.expected.iter().map(|hex_id| hex_to_id(hex_id))) + .collect(); + Ok((store, tips, expected)) + } + + fn setup_commitgraph(&self, store: &gix_odb::Store, use_graph: bool) -> Option { + use_graph + .then(|| gix_commitgraph::at(store.path().join("info"))) + .transpose() + .expect("graph can be loaded if it exists") + } + + fn check_with_predicate(&mut self, predicate: impl FnMut(&oid) -> bool + Clone) -> crate::Result<()> { + let (store, tips, expected) = self.setup()?; + + for use_commitgraph in [false, true] { + let oids = commit::Simple::filtered(tips.clone(), &store, predicate.clone()) + .sorting(self.sorting)? + .parents(self.mode) + .commit_graph(self.setup_commitgraph(store.store_ref(), use_commitgraph)) + .map(|res| res.map(|info| info.id)) + .collect::, _>>()?; + + assert_eq!(oids, expected); + } + Ok(()) + } + + fn check(&self) -> crate::Result { + let (store, tips, expected) = self.setup()?; + + for use_commitgraph in [false, true] { + let oids = commit::Simple::new(tips.clone(), &store) + .sorting(self.sorting)? + .parents(self.mode) + .commit_graph(self.setup_commitgraph(store.store_ref(), use_commitgraph)) + .map(|res| res.map(|info| info.id)) + .collect::, _>>()?; + assert_eq!(oids, expected); + } + Ok(()) + } +} + +mod different_date_intermixed { + use gix_traverse::commit::simple::Sorting; + + use crate::commit::simple::TraversalAssertion; + + #[test] + fn head_breadth_first() -> crate::Result { + TraversalAssertion::new_at( + "make_repos.sh", + "intermixed", + &["58912d92944087dcb09dca79cdd2a937cc158bed"], /* merge */ + // This is very different from what git does as it keeps commits together, + // whereas we spread them out breadth-first. + &[ + "2dce37be587e07caef8c4a5ab60b423b13a8536a", /* c3 */ + "0f6632a5a7d81417488b86692b729e49c1b73056", /* b1c2 */ + "a9c28710e058af4e5163699960234adb9fb2abc7", /* b2c2 */ + "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ + "77fd3c6832c0cd542f7a39f3af9250c3268db979", /* b1c1 */ + "b648f955b930ca95352fae6f22cb593ee0244b27", /* b2c1 */ + "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ + ], + ) + .check() + } + + #[test] + fn head_date_order() -> crate::Result { + TraversalAssertion::new_at( + "make_repos.sh", + "intermixed", + &["58912d92944087dcb09dca79cdd2a937cc158bed"], /* merge */ + // This is exactly what git shows. + &[ + "2dce37be587e07caef8c4a5ab60b423b13a8536a", /* c3 */ + "0f6632a5a7d81417488b86692b729e49c1b73056", /* b1c2 */ + "a9c28710e058af4e5163699960234adb9fb2abc7", /* b2c2 */ + "77fd3c6832c0cd542f7a39f3af9250c3268db979", /* b1c1 */ + "b648f955b930ca95352fae6f22cb593ee0244b27", /* b2c1 */ + "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ + "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ + ], + ) + .with_sorting(Sorting::ByCommitTimeNewestFirst) + .check() + } +} + +mod different_date { + use gix_traverse::commit::simple::Sorting; + + use crate::commit::simple::TraversalAssertion; + + #[test] + fn head_breadth_first() -> crate::Result { + TraversalAssertion::new_at( + "make_repos.sh", + "simple", + &["f49838d84281c3988eeadd988d97dd358c9f9dc4"], /* merge */ + // This is very different from what git does as it keeps commits together, + // whereas we spread them out breadth-first. + &[ + "0edb95c0c0d9933d88f532ec08fcd405d0eee882", /* c5 */ + "66a309480201c4157b0eae86da69f2d606aadbe7", /* b1c2 */ + "48e8dac19508f4238f06c8de2b10301ce64a641c", /* b2c2 */ + "8cb5f13b66ce52a49399a2c49f537ee2b812369c", /* c4 */ + "80947acb398362d8236fcb8bf0f8a9dac640583f", /* b1c1 */ + "cb6a6befc0a852ac74d74e0354e0f004af29cb79", /* b2c1 */ + "33aa07785dd667c0196064e3be3c51dd9b4744ef", /* c3 */ + "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ + "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ + ], + ) + .check() + } + + #[test] + fn head_date_order() -> crate::Result { + TraversalAssertion::new_at( + "make_repos.sh", + "simple", + &["f49838d84281c3988eeadd988d97dd358c9f9dc4"], /* merge */ + // This is exactly what git shows. + &[ + "0edb95c0c0d9933d88f532ec08fcd405d0eee882", /* c5 */ + "66a309480201c4157b0eae86da69f2d606aadbe7", /* b1c2 */ + "80947acb398362d8236fcb8bf0f8a9dac640583f", /* b1c1 */ + "48e8dac19508f4238f06c8de2b10301ce64a641c", /* b2c2 */ + "cb6a6befc0a852ac74d74e0354e0f004af29cb79", /* b2c1 */ + "8cb5f13b66ce52a49399a2c49f537ee2b812369c", /* c4 */ + "33aa07785dd667c0196064e3be3c51dd9b4744ef", /* c3 */ + "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ + "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ + ], + ) + .with_sorting(Sorting::ByCommitTimeNewestFirst) + .check() + } +} + +/// Same dates are somewhat special as they show how sorting-details on priority queues affects ordering +mod same_date { + use gix_traverse::commit::{simple::Sorting, Parents}; + + use crate::{commit::simple::TraversalAssertion, hex_to_id}; + + #[test] + fn c4_breadth_first() -> crate::Result { + TraversalAssertion::new( + "make_traversal_repo_for_commits_same_date.sh", + &["9556057aee5abb06912922e9f26c46386a816822"], /* c4 */ + &[ + "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ + "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ + "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ + ], + ) + .check() + } + + #[test] + fn head_breadth_first() -> crate::Result { + TraversalAssertion::new( + "make_traversal_repo_for_commits_same_date.sh", + &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ + // We always take the first parent first, then the second, and so on. + // Deviation: git for some reason displays b1c2 *before* c5, but I think it's better + // to have a strict parent order. + &[ + "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ + "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ + "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ + "9152eeee2328073cf23dcf8e90c949170b711659", /* b1c1 */ + "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ + "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ + "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ + ], + ) + .check() + } + + #[test] + fn head_date_order() -> crate::Result { + TraversalAssertion::new( + "make_traversal_repo_for_commits_same_date.sh", + &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ + &[ + "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ + "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ + "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ + "9152eeee2328073cf23dcf8e90c949170b711659", /* b1c1 */ + "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ + "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ + "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ + ], + ) + .with_sorting(Sorting::ByCommitTimeNewestFirst) + .check() + } + + #[test] + fn head_first_parent_only_breadth_first() -> crate::Result { + TraversalAssertion::new( + "make_traversal_repo_for_commits_same_date.sh", + &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ + &[ + "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ + "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ + "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ + "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ + "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ + ], + ) + .with_parents(Parents::First) + .check() + } + + #[test] + fn head_c4_breadth_first() -> crate::Result { + TraversalAssertion::new( + "make_traversal_repo_for_commits_same_date.sh", + &[ + "01ec18a3ebf2855708ad3c9d244306bc1fae3e9b", /* m1b1 */ + "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ + ], + &[ + "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ + "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ + "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ + "9152eeee2328073cf23dcf8e90c949170b711659", /* b1c1 */ + "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ + "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ + ], + ) + .check() + } + + #[test] + fn filtered_commit_does_not_block_ancestors_reachable_from_another_commit() -> crate::Result { + // I don't see a use case for the predicate returning false for a commit but return true for + // at least one of its ancestors, so this test is kind of dubious. But we do want + // `Ancestors` to not eagerly blacklist all of a commit's ancestors when blacklisting that + // one commit, and this test happens to check that. + TraversalAssertion::new( + "make_traversal_repo_for_commits_same_date.sh", + &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ + &[ + "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ + "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ + "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ + "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ + "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ + "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ + ], + ) + .check_with_predicate(|id| id != hex_to_id("9152eeee2328073cf23dcf8e90c949170b711659")) + } + + #[test] + fn predicate_only_called_once_even_if_fork_point() -> crate::Result { + // The `self.seen` check should come before the `self.predicate` check, as we don't know how + // expensive calling `self.predicate` may be. + let mut seen = false; + TraversalAssertion::new( + "make_traversal_repo_for_commits_same_date.sh", + &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ + &[ + "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ + "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ + "9152eeee2328073cf23dcf8e90c949170b711659", /* b1c1 */ + ], + ) + .check_with_predicate(move |id| { + if id == hex_to_id("9556057aee5abb06912922e9f26c46386a816822") { + assert!(!seen); + seen = true; + false + } else { + true + } + }) + } +} + +/// Some dates adjusted to be a year apart, but still 'c1' and 'c2' with the same date. +mod adjusted_dates { + use gix_traverse::commit::{simple::Sorting, Parents, Simple}; + + use crate::{commit::simple::TraversalAssertion, hex_to_id}; + + #[test] + fn head_breadth_first() -> crate::Result { + TraversalAssertion::new( + "make_traversal_repo_for_commits_with_dates.sh", + &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ + // Here `git` also shows `b1c1` first, making topo-order similar to date order for some reason, + // even though c2 *is* the first parent. + &[ + "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ + "bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac", /* b1c1 */ + "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ + ], + ) + .check() + } + + #[test] + fn head_date_order() -> crate::Result { + TraversalAssertion::new( + "make_traversal_repo_for_commits_with_dates.sh", + &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ + &[ + "bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac", /* b1c1 */ + "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ + "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ + ], + ) + .with_sorting(Sorting::ByCommitTimeNewestFirst) + .check() + } + + #[test] + fn head_date_order_with_cutoff() -> crate::Result { + TraversalAssertion::new( + "make_traversal_repo_for_commits_with_dates.sh", + &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ + &["bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac"], /* b1c1 */ + ) + .with_sorting(Sorting::ByCommitTimeNewestFirstCutoffOlderThan { + seconds: 978393600, // =2001-01-02 00:00:00 +0000 + }) + .check() + } + + #[test] + fn date_order_with_cutoff_is_applied_to_starting_position() -> crate::Result { + let dir = + gix_testtools::scripted_fixture_read_only_standalone("make_traversal_repo_for_commits_with_dates.sh")?; + let store = gix_odb::at(dir.join(".git").join("objects"))?; + let iter = Simple::new( + Some(hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7" /* c2 */)), + &store, + ) + .sorting(Sorting::ByCommitTimeNewestFirstCutoffOlderThan { + seconds: 978393600, // =2001-01-02 00:00:00 +0000 + })?; + assert_eq!( + iter.count(), + 0, + "initial tips that don't pass cutoff value are not returned either" + ); + Ok(()) + } + + #[test] + fn head_date_order_first_parent_only() -> crate::Result { + TraversalAssertion::new( + "make_traversal_repo_for_commits_with_dates.sh", + &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ + &[ + "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ + "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ + ], + ) + .with_sorting(Sorting::ByCommitTimeNewestFirst) + .with_parents(Parents::First) + .check() + } +} diff --git a/gix-traverse/tests/commit/topo.rs b/gix-traverse/tests/commit/topo.rs new file mode 100644 index 00000000000..ff28a0b0c95 --- /dev/null +++ b/gix-traverse/tests/commit/topo.rs @@ -0,0 +1,372 @@ +use gix_hash::{oid, ObjectId}; +use gix_object::bstr::ByteSlice; +use gix_traverse::commit::{topo, Parents}; +use std::path::PathBuf; + +use crate::hex_to_id; + +struct TraversalAssertion<'a> { + init_script: &'a str, + worktree_dir: PathBuf, + repo_name: &'a str, + tips: &'a [&'a str], + ends: &'a [&'a str], + expected: &'a [&'a str], + mode: Parents, + sorting: topo::Sorting, +} + +/// API +impl<'a> TraversalAssertion<'a> { + fn new(tips: &'a [&'a str], ends: &'a [&'a str], expected: &'a [&'a str]) -> Self { + Self::new_at("make_repo_for_topo.sh", "", tips, ends, expected) + } + + fn new_at( + init_script: &'a str, + repo_name: &'a str, + tips: &'a [&'a str], + ends: &'a [&'a str], + expected: &'a [&'a str], + ) -> Self { + TraversalAssertion { + init_script, + worktree_dir: Default::default(), + repo_name, + tips, + ends, + expected, + mode: Default::default(), + sorting: Default::default(), + } + } + + fn with_parents(&mut self, mode: Parents) -> &mut Self { + self.mode = mode; + self + } + + fn with_sorting(&mut self, sorting: topo::Sorting) -> &mut Self { + self.sorting = sorting; + self + } + + fn check_with_predicate(&mut self, predicate: impl FnMut(&oid) -> bool + Clone) -> crate::Result<()> { + let (store, tips, ends, expected) = self.setup()?; + + for use_commitgraph in [false, true] { + let oids = topo::Builder::from_iters(&store, tips.iter().copied(), Some(ends.iter().copied())) + .sorting(self.sorting) + .with_commit_graph(self.setup_commitgraph(store.store_ref(), use_commitgraph)) + .parents(self.mode) + .with_predicate(predicate.clone()) + .build()? + .map(|res| res.map(|info| info.id)) + .collect::, _>>()?; + + assert_eq!(oids, expected); + } + Ok(()) + } + + fn assert_baseline(&self, name: &str) { + let buf = std::fs::read(self.worktree_dir.join(format!("{name}.baseline"))) + .expect("a baseline must be set for each repo"); + let expected: Vec<_> = buf.lines().map(|s| s.to_str().unwrap()).collect(); + assert_eq!( + self.expected, expected, + "Baseline must match the expectation we provide here" + ); + } + + fn check(&mut self) -> crate::Result { + let (store, tips, ends, expected) = self.setup()?; + + for use_commitgraph in [false, true] { + let oids = topo::Builder::from_iters(&store, tips.iter().copied(), Some(ends.iter().copied())) + .sorting(self.sorting) + .with_commit_graph(self.setup_commitgraph(store.store_ref(), use_commitgraph)) + .parents(self.mode) + .build()? + .map(|res| res.map(|info| info.id)) + .collect::, _>>()?; + + assert_eq!(oids, expected); + } + Ok(()) + } +} + +impl TraversalAssertion<'_> { + #[allow(clippy::type_complexity)] + fn setup(&mut self) -> crate::Result<(gix_odb::Handle, Vec, Vec, Vec)> { + let dir = gix_testtools::scripted_fixture_read_only_standalone(self.init_script)?; + let worktree_dir = dir.join(self.repo_name); + let store = gix_odb::at(worktree_dir.join(".git").join("objects"))?; + self.worktree_dir = worktree_dir; + + let tips: Vec<_> = self.tips.iter().copied().map(hex_to_id).collect(); + let ends: Vec<_> = self.ends.iter().copied().map(hex_to_id).collect(); + // `tips` is not chained with expected unlike in `commit`'s + // TraversalAssertion since it's not given that all the tips are + // shown first. + let expected: Vec = self.expected.iter().map(|hex_id| hex_to_id(hex_id)).collect(); + + Ok((store, tips, ends, expected)) + } + + fn setup_commitgraph(&self, store: &gix_odb::Store, use_graph: bool) -> Option { + use_graph + .then(|| gix_commitgraph::at(store.path().join("info"))) + .transpose() + .expect("graph can be loaded if it exists") + } +} + +mod basic { + use gix_traverse::commit::topo; + + use super::TraversalAssertion; + + use crate::hex_to_id; + + #[test] + fn simple() -> crate::Result { + let mut assertion = TraversalAssertion::new( + &["62ed296d9986f50477e9f7b7e81cd0258939a43d"], + &[], + &[ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "3be0c4c793c634c8fd95054345d4935d10a0879a", + "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", + "302a5d0530ec688c241f32c2f2b61b964dd17bee", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + "f1cce1b5c7efcdfa106e95caa6c45a2cae48a481", + "945d8a360915631ad545e0cf04630d86d3d4eaa1", + "a863c02247a6c5ba32dff5224459f52aa7f77f7b", + "2f291881edfb0597493a52d26ea09dd7340ce507", + "9c46b8765703273feb10a2ebd810e70b8e2ca44a", + "fb3e21cf45b04b617011d2b30973f3e5ce60d0cd", + ], + ); + assertion.with_sorting(topo::Sorting::TopoOrder).check()?; + assertion.assert_baseline("all-commits"); + Ok(()) + } + + #[test] + fn one_end() -> crate::Result { + TraversalAssertion::new( + &["62ed296d9986f50477e9f7b7e81cd0258939a43d"], + &["f1cce1b5c7efcdfa106e95caa6c45a2cae48a481"], + &[ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "3be0c4c793c634c8fd95054345d4935d10a0879a", + "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", + "302a5d0530ec688c241f32c2f2b61b964dd17bee", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + ], + ) + .with_sorting(topo::Sorting::TopoOrder) + .check() + } + + #[test] + fn empty_range() -> crate::Result { + TraversalAssertion::new( + &["f1cce1b5c7efcdfa106e95caa6c45a2cae48a481"], + &["eeab3243aad67bc838fc4425f759453bf0b47785"], + &[], + ) + .with_sorting(topo::Sorting::TopoOrder) + .check() + } + + #[test] + fn two_tips_two_ends() -> crate::Result { + TraversalAssertion::new( + &[ + "d09384f312b03e4a1413160739805ff25e8fe99d", + "3be0c4c793c634c8fd95054345d4935d10a0879a", + ], + &[ + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + ], + &[ + "3be0c4c793c634c8fd95054345d4935d10a0879a", + "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", + "302a5d0530ec688c241f32c2f2b61b964dd17bee", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + ], + ) + .with_sorting(topo::Sorting::TopoOrder) + .check() + } + + #[test] + fn with_dummy_predicate() -> crate::Result { + TraversalAssertion::new( + &["62ed296d9986f50477e9f7b7e81cd0258939a43d"], + &[], + &[ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "3be0c4c793c634c8fd95054345d4935d10a0879a", + "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", + "302a5d0530ec688c241f32c2f2b61b964dd17bee", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + "f1cce1b5c7efcdfa106e95caa6c45a2cae48a481", + "945d8a360915631ad545e0cf04630d86d3d4eaa1", + "a863c02247a6c5ba32dff5224459f52aa7f77f7b", + "2f291881edfb0597493a52d26ea09dd7340ce507", + "9c46b8765703273feb10a2ebd810e70b8e2ca44a", + "fb3e21cf45b04b617011d2b30973f3e5ce60d0cd", + ], + ) + .with_sorting(topo::Sorting::TopoOrder) + .check_with_predicate(|oid| oid != hex_to_id("eeab3243aad67bc838fc4425f759453bf0b47785")) + } + + #[test] + fn end_along_first_parent() -> crate::Result { + TraversalAssertion::new( + &["d09384f312b03e4a1413160739805ff25e8fe99d"], + &["33eb18340e4eaae3e3dcf80222b02f161cd3f966"], + &[ + "d09384f312b03e4a1413160739805ff25e8fe99d", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + ], + ) + .with_sorting(topo::Sorting::TopoOrder) + .check() + } +} + +mod first_parent { + use gix_traverse::commit::{topo, Parents}; + + use super::TraversalAssertion; + + #[test] + fn basic() -> crate::Result { + let mut assertion = TraversalAssertion::new( + &["62ed296d9986f50477e9f7b7e81cd0258939a43d"], + &[], + &[ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + "f1cce1b5c7efcdfa106e95caa6c45a2cae48a481", + "945d8a360915631ad545e0cf04630d86d3d4eaa1", + "a863c02247a6c5ba32dff5224459f52aa7f77f7b", + "2f291881edfb0597493a52d26ea09dd7340ce507", + "9c46b8765703273feb10a2ebd810e70b8e2ca44a", + "fb3e21cf45b04b617011d2b30973f3e5ce60d0cd", + ], + ); + assertion + .with_parents(Parents::First) + .with_sorting(topo::Sorting::TopoOrder) + .check()?; + + assertion.assert_baseline("first-parent"); + Ok(()) + } + + #[test] + fn with_end() -> crate::Result { + TraversalAssertion::new( + &["62ed296d9986f50477e9f7b7e81cd0258939a43d"], + &["f1cce1b5c7efcdfa106e95caa6c45a2cae48a481"], + &[ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + ], + ) + .with_parents(Parents::First) + .with_sorting(topo::Sorting::TopoOrder) + .check() + } + + #[test] + fn end_is_second_parent() -> crate::Result { + TraversalAssertion::new( + &["62ed296d9986f50477e9f7b7e81cd0258939a43d"], + &["3be0c4c793c634c8fd95054345d4935d10a0879a"], + &[ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + ], + ) + .with_parents(Parents::First) + .with_sorting(topo::Sorting::TopoOrder) + .check() + } +} + +mod date_order { + use gix_traverse::commit::topo; + + use super::TraversalAssertion; + + #[test] + fn simple() -> crate::Result { + TraversalAssertion::new( + // Same tip and end as basic::one_end() but the order should be + // different. + &["62ed296d9986f50477e9f7b7e81cd0258939a43d"], + &["f1cce1b5c7efcdfa106e95caa6c45a2cae48a481"], + &[ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "3be0c4c793c634c8fd95054345d4935d10a0879a", + "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", + "302a5d0530ec688c241f32c2f2b61b964dd17bee", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + ], + ) + .with_sorting(topo::Sorting::DateOrder) + .check() + } +} diff --git a/gix-traverse/tests/fixtures/generated-archives/make_repo_for_topo.tar.xz b/gix-traverse/tests/fixtures/generated-archives/make_repo_for_topo.tar.xz new file mode 100644 index 00000000000..12a8797c097 Binary files /dev/null and b/gix-traverse/tests/fixtures/generated-archives/make_repo_for_topo.tar.xz differ diff --git a/gix-traverse/tests/fixtures/make_repo_for_topo.sh b/gix-traverse/tests/fixtures/make_repo_for_topo.sh new file mode 100755 index 00000000000..6550da2e2c0 --- /dev/null +++ b/gix-traverse/tests/fixtures/make_repo_for_topo.sh @@ -0,0 +1,66 @@ +#!/bin/bash +set -eu -o pipefail + +function tick () { + if test -z "${tick+set}" + then + tick=1112911993 + else + tick=$(($tick + 60)) + fi + GIT_COMMITTER_DATE="$tick -0700" + GIT_AUTHOR_DATE="$tick -0700" + export GIT_COMMITTER_DATE GIT_AUTHOR_DATE +} + +tick +function commit() { + local message=${1:?first argument is the commit message} + tick + git commit --allow-empty -m "$message" +} + +function optimize() { + git commit-graph write --no-progress --reachable + git repack -adq +} + +function collect_baselines() { + git rev-list --topo-order HEAD > all-commits.baseline + git rev-list --topo-order --first-parent HEAD > first-parent.baseline +} + +git init +git config merge.ff false + +git checkout -q -b main +for i in {0..5}; do + commit c$i +done + +git branch branch1 +for i in {6..8}; do + commit c$i +done + +git checkout -q branch1 +commit b1c1 + +git checkout -q main +commit c9 + +git merge branch1 -m merge + +git checkout -q branch1 +commit c10 +commit c11 + +git checkout -q branch1 +commit b1c2 + +git checkout -q main +git merge branch1 -m merge +commit c12 + +optimize +collect_baselines diff --git a/gix/src/config/cache/util.rs b/gix/src/config/cache/util.rs index 4c1d6c69399..df120fb75d8 100644 --- a/gix/src/config/cache/util.rs +++ b/gix/src/config/cache/util.rs @@ -132,10 +132,6 @@ pub trait ApplyLeniency { fn with_leniency(self, is_lenient: bool) -> Self; } -pub trait IgnoreEmptyPath { - fn ignore_empty(self) -> Self; -} - pub trait ApplyLeniencyDefault { fn with_lenient_default(self, is_lenient: bool) -> Self; } @@ -154,16 +150,6 @@ impl ApplyLeniency for Result, E> { } } -impl IgnoreEmptyPath for Result>, gix_config::path::interpolate::Error> { - fn ignore_empty(self) -> Self { - match self { - Ok(maybe_path) => Ok(maybe_path), - Err(gix_config::path::interpolate::Error::Missing { .. }) => Ok(None), - Err(err) => Err(err), - } - } -} - impl ApplyLeniencyDefault for Result where T: Default, diff --git a/gix/src/config/snapshot/credential_helpers.rs b/gix/src/config/snapshot/credential_helpers.rs index fdac608f541..f84efa896e8 100644 --- a/gix/src/config/snapshot/credential_helpers.rs +++ b/gix/src/config/snapshot/credential_helpers.rs @@ -6,7 +6,6 @@ use crate::config::cache::util::ApplyLeniency; use crate::{ bstr::{ByteSlice, ByteVec}, config::{ - cache::util::IgnoreEmptyPath, tree::{credential, gitoxide::Credentials, Core, Credential, Key}, Snapshot, }, @@ -197,3 +196,17 @@ fn normalize(url: &mut gix_url::Url) { url.path.pop(); } } + +trait IgnoreEmptyPath { + fn ignore_empty(self) -> Self; +} + +impl IgnoreEmptyPath for Result>, gix_config::path::interpolate::Error> { + fn ignore_empty(self) -> Self { + match self { + Ok(maybe_path) => Ok(maybe_path), + Err(gix_config::path::interpolate::Error::Missing { .. }) => Ok(None), + Err(err) => Err(err), + } + } +} diff --git a/gix/src/ext/object_id.rs b/gix/src/ext/object_id.rs index d4d9467664b..d7f91164170 100644 --- a/gix/src/ext/object_id.rs +++ b/gix/src/ext/object_id.rs @@ -1,9 +1,8 @@ use gix_hash::ObjectId; -use gix_traverse::commit::{ancestors, Ancestors}; pub trait Sealed {} -pub type AncestorsIter = Ancestors bool, ancestors::State>; +pub type AncestorsIter = gix_traverse::commit::Simple bool>; /// An extension trait to add functionality to [`ObjectId`]s. pub trait ObjectIdExt: Sealed { @@ -23,7 +22,7 @@ impl ObjectIdExt for ObjectId { where Find: gix_object::Find, { - Ancestors::new(Some(self), ancestors::State::default(), find) + gix_traverse::commit::Simple::new(Some(self), find) } fn attach(self, repo: &crate::Repository) -> crate::Id<'_> { diff --git a/gix/src/remote/connection/fetch/update_refs/mod.rs b/gix/src/remote/connection/fetch/update_refs/mod.rs index 06a74fb3cbc..e5d3eac1b2d 100644 --- a/gix/src/remote/connection/fetch/update_refs/mod.rs +++ b/gix/src/remote/connection/fetch/update_refs/mod.rs @@ -162,7 +162,7 @@ pub(crate) fn update( .to_owned() .ancestors(&repo.objects) .sorting( - gix_traverse::commit::Sorting::ByCommitTimeNewestFirstCutoffOlderThan { + gix_traverse::commit::simple::Sorting::ByCommitTimeNewestFirstCutoffOlderThan { seconds: local_commit_time }, ) diff --git a/gix/src/revision/spec/parse/delegate/navigate.rs b/gix/src/revision/spec/parse/delegate/navigate.rs index acd8ecc530b..43f40654f40 100644 --- a/gix/src/revision/spec/parse/delegate/navigate.rs +++ b/gix/src/revision/spec/parse/delegate/navigate.rs @@ -6,7 +6,6 @@ use gix_revision::spec::parse::{ delegate, delegate::{PeelTo, Traversal}, }; -use gix_traverse::commit::Sorting; use crate::{ bstr::{BStr, ByteSlice}, @@ -193,7 +192,7 @@ impl<'repo> delegate::Navigate for Delegate<'repo> { match oid .attach(repo) .ancestors() - .sorting(Sorting::ByCommitTimeNewestFirst) + .sorting(gix_traverse::commit::simple::Sorting::ByCommitTimeNewestFirst) .all() { Ok(iter) => { @@ -242,15 +241,10 @@ impl<'repo> delegate::Navigate for Delegate<'repo> { references .peeled() .filter_map(Result::ok) - .filter(|r| { - r.id() - .object() - .ok() - .map_or(false, |obj| obj.kind == gix_object::Kind::Commit) - }) + .filter(|r| r.id().header().ok().map_or(false, |obj| obj.kind().is_commit())) .filter_map(|r| r.detach().peeled), ) - .sorting(Sorting::ByCommitTimeNewestFirst) + .sorting(gix_traverse::commit::simple::Sorting::ByCommitTimeNewestFirst) .all() { Ok(iter) => { diff --git a/gix/src/revision/spec/parse/types.rs b/gix/src/revision/spec/parse/types.rs index fc09e13c097..9629b0b34d4 100644 --- a/gix/src/revision/spec/parse/types.rs +++ b/gix/src/revision/spec/parse/types.rs @@ -184,7 +184,7 @@ pub enum Error { next: Option>, }, #[error(transparent)] - Traverse(#[from] gix_traverse::commit::ancestors::Error), + Traverse(#[from] gix_traverse::commit::simple::Error), #[error(transparent)] Walk(#[from] crate::revision::walk::Error), #[error("Spec does not contain a single object id")] diff --git a/gix/src/revision/walk.rs b/gix/src/revision/walk.rs index 19d15d569ab..dd7c5a8c1dc 100644 --- a/gix/src/revision/walk.rs +++ b/gix/src/revision/walk.rs @@ -8,7 +8,7 @@ use crate::{ext::ObjectIdExt, revision, Repository}; #[allow(missing_docs)] pub enum Error { #[error(transparent)] - AncestorIter(#[from] gix_traverse::commit::ancestors::Error), + SimpleTraversal(#[from] gix_traverse::commit::simple::Error), #[error(transparent)] ShallowCommits(#[from] crate::shallow::open::Error), #[error(transparent)] @@ -22,8 +22,8 @@ pub struct Info<'repo> { pub id: gix_hash::ObjectId, /// All parent ids we have encountered. Note that these will be at most one if [`Parents::First`][gix_traverse::commit::Parents::First] is enabled. pub parent_ids: gix_traverse::commit::ParentIds, - /// The time at which the commit was created. It's only `Some(_)` if sorting is not [`Sorting::BreadthFirst`][gix_traverse::commit::Sorting::BreadthFirst], - /// as the walk needs to require the commit-date. + /// The time at which the commit was created. It will only be `Some(_)` if the chosen traversal was + /// taking dates into consideration. pub commit_time: Option, repo: &'repo Repository, @@ -91,7 +91,7 @@ impl<'repo> Info<'repo> { pub struct Platform<'repo> { pub(crate) repo: &'repo Repository, pub(crate) tips: Vec, - pub(crate) sorting: gix_traverse::commit::Sorting, + pub(crate) sorting: gix_traverse::commit::simple::Sorting, pub(crate) parents: gix_traverse::commit::Parents, pub(crate) use_commit_graph: Option, pub(crate) commit_graph: Option, @@ -113,7 +113,7 @@ impl<'repo> Platform<'repo> { /// Create-time builder methods impl<'repo> Platform<'repo> { /// Set the sort mode for commits to the given value. The default is to order topologically breadth-first. - pub fn sorting(mut self, sorting: gix_traverse::commit::Sorting) -> Self { + pub fn sorting(mut self, sorting: gix_traverse::commit::simple::Sorting) -> Self { self.sorting = sorting; self } @@ -166,40 +166,35 @@ impl<'repo> Platform<'repo> { Ok(revision::Walk { repo, inner: Box::new( - gix_traverse::commit::Ancestors::filtered( - tips, - gix_traverse::commit::ancestors::State::default(), - &repo.objects, - { - // Note that specific shallow handling for commit-graphs isn't needed as these contain - // all information there is, and exclude shallow parents to be structurally consistent. - let shallow_commits = repo.shallow_commits()?; - let mut grafted_parents_to_skip = Vec::new(); - let mut buf = Vec::new(); - move |id| { - if !filter(id) { - return false; - } - match shallow_commits.as_ref() { - Some(commits) => { - let id = id.to_owned(); - if let Ok(idx) = grafted_parents_to_skip.binary_search(&id) { - grafted_parents_to_skip.remove(idx); - return false; - }; - if commits.binary_search(&id).is_ok() { - if let Ok(commit) = repo.objects.find_commit_iter(&id, &mut buf) { - grafted_parents_to_skip.extend(commit.parent_ids()); - grafted_parents_to_skip.sort(); - } - }; - true - } - None => true, + gix_traverse::commit::Simple::filtered(tips, &repo.objects, { + // Note that specific shallow handling for commit-graphs isn't needed as these contain + // all information there is, and exclude shallow parents to be structurally consistent. + let shallow_commits = repo.shallow_commits()?; + let mut grafted_parents_to_skip = Vec::new(); + let mut buf = Vec::new(); + move |id| { + if !filter(id) { + return false; + } + match shallow_commits.as_ref() { + Some(commits) => { + let id = id.to_owned(); + if let Ok(idx) = grafted_parents_to_skip.binary_search(&id) { + grafted_parents_to_skip.remove(idx); + return false; + }; + if commits.binary_search(&id).is_ok() { + if let Ok(commit) = repo.objects.find_commit_iter(&id, &mut buf) { + grafted_parents_to_skip.extend(commit.parent_ids()); + grafted_parents_to_skip.sort(); + } + }; + true } + None => true, } - }, - ) + } + }) .sorting(sorting)? .parents(parents) .commit_graph( @@ -226,13 +221,12 @@ pub(crate) mod iter { /// The iterator returned by [`crate::revision::walk::Platform::all()`]. pub struct Walk<'repo> { pub(crate) repo: &'repo crate::Repository, - pub(crate) inner: Box< - dyn Iterator> + 'repo, - >, + pub(crate) inner: + Box> + 'repo>, } impl<'repo> Iterator for Walk<'repo> { - type Item = Result, gix_traverse::commit::ancestors::Error>; + type Item = Result, gix_traverse::commit::simple::Error>; fn next(&mut self) -> Option { self.inner diff --git a/gix/tests/id/mod.rs b/gix/tests/id/mod.rs index f284a76e65a..143881d92c9 100644 --- a/gix/tests/id/mod.rs +++ b/gix/tests/id/mod.rs @@ -87,7 +87,7 @@ mod ancestors { let commits_by_commit_date = head .ancestors() .use_commit_graph(!use_commit_graph) - .sorting(commit::Sorting::ByCommitTimeNewestFirst) + .sorting(commit::simple::Sorting::ByCommitTimeNewestFirst) .all()? .map(|c| c.map(gix::revision::walk::Info::detach)) .collect::, _>>()?; @@ -121,7 +121,7 @@ mod ancestors { let head = repo.head()?.into_peeled_id()?; let commits = head .ancestors() - .sorting(commit::Sorting::ByCommitTimeNewestFirst) // assure we have time set + .sorting(commit::simple::Sorting::ByCommitTimeNewestFirst) // assure we have time set .use_commit_graph(use_commit_graph) .all()? .collect::, _>>()?; @@ -141,9 +141,9 @@ mod ancestors { for use_commit_graph in [false, true] { for sorting in [ - commit::Sorting::BreadthFirst, - commit::Sorting::ByCommitTimeNewestFirst, - commit::Sorting::ByCommitTimeNewestFirstCutoffOlderThan { seconds: 0 }, + commit::simple::Sorting::BreadthFirst, + commit::simple::Sorting::ByCommitTimeNewestFirst, + commit::simple::Sorting::ByCommitTimeNewestFirstCutoffOlderThan { seconds: 0 }, ] { let commits_graph_order = head .ancestors() diff --git a/gix/tests/repository/config/mod.rs b/gix/tests/repository/config/mod.rs index 056bc903546..227a7ed308e 100644 --- a/gix/tests/repository/config/mod.rs +++ b/gix/tests/repository/config/mod.rs @@ -38,7 +38,6 @@ mod ssh_options { #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] mod transport_options; -#[cfg(feature = "blocking-network-client")] #[cfg(feature = "blocking-network-client")] pub fn repo(name: &str) -> gix::Repository { repo_opts(name, |opts| opts.strict_config(true)) diff --git a/gix/tests/repository/shallow.rs b/gix/tests/repository/shallow.rs index aac1ee5cc37..8e5c2bd0a66 100644 --- a/gix/tests/repository/shallow.rs +++ b/gix/tests/repository/shallow.rs @@ -44,7 +44,7 @@ fn yes() -> crate::Result { } mod traverse { - use gix_traverse::commit::Sorting; + use gix_traverse::commit::simple::Sorting; use serial_test::parallel; use crate::util::{hex_to_id, named_subrepo_opts};