From ecc049cf14187d751c4d33b4c4b5d71bcce620b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98ystein=20Walle?= Date: Mon, 26 Feb 2024 20:04:33 +0100 Subject: [PATCH 01/10] Move [Aa]ncestors to under commit module In preparation of a new module for topological commit walk. The idea is that the current walk will be the "default" hence the 'pub use'. --- gix-traverse/src/{commit.rs => commit/ancestors.rs} | 4 +++- gix-traverse/src/commit/mod.rs | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) rename gix-traverse/src/{commit.rs => commit/ancestors.rs} (99%) create mode 100644 gix-traverse/src/commit/mod.rs diff --git a/gix-traverse/src/commit.rs b/gix-traverse/src/commit/ancestors.rs similarity index 99% rename from gix-traverse/src/commit.rs rename to gix-traverse/src/commit/ancestors.rs index c1d18ebf6ff..236b0365119 100644 --- a/gix-traverse/src/commit.rs +++ b/gix-traverse/src/commit/ancestors.rs @@ -100,7 +100,7 @@ pub mod ancestors { use gix_object::{CommitRefIter, FindExt}; use smallvec::SmallVec; - use crate::commit::{collect_parents, Ancestors, Either, Info, ParentIds, Parents, Sorting}; + use super::{collect_parents, Ancestors, Either, Info, ParentIds, Parents, Sorting}; /// The error is part of the item returned by the [Ancestors] iterator. #[derive(Debug, thiserror::Error)] @@ -455,6 +455,8 @@ pub mod ancestors { } } +pub use ancestors::{Error, State}; + enum Either<'buf, 'cache> { CommitRefIter(gix_object::CommitRefIter<'buf>), CachedCommit(gix_commitgraph::file::Commit<'cache>), diff --git a/gix-traverse/src/commit/mod.rs b/gix-traverse/src/commit/mod.rs new file mode 100644 index 00000000000..1971d2103ae --- /dev/null +++ b/gix-traverse/src/commit/mod.rs @@ -0,0 +1,3 @@ +/// Simple ancestors traversal +pub mod ancestors; +pub use ancestors::{Ancestors, Info, ParentIds, Parents, Sorting}; From 5b3dfbb129ae5e53fd63ce1a8637a35d7836b909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98ystein=20Walle?= Date: Sat, 7 Oct 2023 13:28:23 +0200 Subject: [PATCH 02/10] Move some utilities to super module to make them sharable Makes it easier to use these types for different kinds of commit walking. No matter how the history graph is traversed some concepts remain identical. --- gix-traverse/src/commit/ancestors.rs | 59 ++++------------------------ gix-traverse/src/commit/mod.rs | 52 +++++++++++++++++++++++- 2 files changed, 58 insertions(+), 53 deletions(-) diff --git a/gix-traverse/src/commit/ancestors.rs b/gix-traverse/src/commit/ancestors.rs index 236b0365119..9d61d80be4d 100644 --- a/gix-traverse/src/commit/ancestors.rs +++ b/gix-traverse/src/commit/ancestors.rs @@ -1,4 +1,3 @@ -use gix_object::FindExt; use smallvec::SmallVec; /// An iterator over the ancestors one or more starting commits @@ -7,20 +6,10 @@ pub struct Ancestors { cache: Option, predicate: Predicate, state: StateMut, - parents: Parents, + parents: super::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. /// /// ### Sample History @@ -69,23 +58,6 @@ 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]>; - -/// 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, -} - /// #[allow(clippy::empty_docs)] pub mod ancestors { @@ -100,7 +72,10 @@ pub mod ancestors { use gix_object::{CommitRefIter, FindExt}; use smallvec::SmallVec; - use super::{collect_parents, Ancestors, Either, Info, ParentIds, Parents, Sorting}; + use super::{ + super::{Either, Info, ParentIds, Parents}, + collect_parents, Ancestors, Sorting, + }; /// The error is part of the item returned by the [Ancestors] iterator. #[derive(Debug, thiserror::Error)] @@ -339,7 +314,7 @@ pub mod ancestors { 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 @@ -406,7 +381,7 @@ pub mod ancestors { let state = self.state.borrow_mut(); 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 @@ -457,11 +432,6 @@ pub mod ancestors { pub use ancestors::{Error, State}; -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>, @@ -483,18 +453,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/mod.rs b/gix-traverse/src/commit/mod.rs index 1971d2103ae..95bb6eb665d 100644 --- a/gix-traverse/src/commit/mod.rs +++ b/gix-traverse/src/commit/mod.rs @@ -1,3 +1,53 @@ +use gix_object::FindExt; +use smallvec::SmallVec; + /// Simple ancestors traversal pub mod ancestors; -pub use ancestors::{Ancestors, Info, ParentIds, Parents, Sorting}; +pub use ancestors::{Ancestors, 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, +} + +/// 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's only `Some(_)` if sorting is not [`Sorting::BreadthFirst`], as the walk + /// needs to require the commit-date. + 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), + } +} From 25b3d23bc370825ee15c68fd869b3ee5f4f79a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98ystein=20Walle?= Date: Sat, 7 Oct 2023 13:29:20 +0200 Subject: [PATCH 03/10] Initial implementation of topological commit walk This commit introduces a new module `topo` in gix-traverse that contains an iterator for walking commits topologically. It is very much based on the existing Git implementation introduced in b45424181e[1] as well as improvements done since then. Function names are kept as much as possible. The mailing list[2] has a lot of interesting discussion about how the algorithm works. In short it is a three stage process that aims to reduce the amount of traversal needed before the first result can be output. Hopefully this makes it easier to use gitoxide to implement other tools that need topological sorting, e.g. graphical log and blame. For tests there is a separate type named TraversalAssertion. There is probably grounds for making code more common in this area. [1]: https://github.com/git/git/commit/b45424181e9e8b2284a48c6db7b8db635bbfccc8 [2]: https://public-inbox.org/git/pull.25.git.gitgitgadget@gmail.com/ Add tests for gix_traverse::topo This unfortunately duplicates types found in the existing tests. --- Cargo.lock | 2 + gix-traverse/Cargo.toml | 2 + gix-traverse/src/commit/mod.rs | 3 + gix-traverse/src/commit/topo.rs | 611 ++++++++++++++++++ .../make_repo_for_topo.tar.xz | Bin 0 -> 12364 bytes .../tests/fixtures/make_repo_for_topo.sh | 60 ++ gix-traverse/tests/topo/mod.rs | 359 ++++++++++ gix-traverse/tests/traverse.rs | 1 + 8 files changed, 1038 insertions(+) create mode 100644 gix-traverse/src/commit/topo.rs create mode 100644 gix-traverse/tests/fixtures/generated-archives/make_repo_for_topo.tar.xz create mode 100755 gix-traverse/tests/fixtures/make_repo_for_topo.sh create mode 100644 gix-traverse/tests/topo/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 23ad26be6b3..887b13ecbf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2678,11 +2678,13 @@ 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", "gix-hashtable 0.5.2", "gix-object 0.42.1", + "gix-revision", "gix-revwalk 0.13.0", "smallvec", "thiserror", diff --git a/gix-traverse/Cargo.toml b/gix-traverse/Cargo.toml index 3024d8eeb15..6cd63dad837 100644 --- a/gix-traverse/Cargo.toml +++ b/gix-traverse/Cargo.toml @@ -20,5 +20,7 @@ gix-date = { version = "^0.8.5", path = "../gix-date" } gix-hashtable = { version = "^0.5.2", path = "../gix-hashtable" } gix-revwalk = { version = "^0.13.0", path = "../gix-revwalk" } gix-commitgraph = { version = "^0.24.2", path = "../gix-commitgraph" } +gix-revision = { version = "^0.27.0", path = "../gix-revision" } 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 index 95bb6eb665d..406999fee65 100644 --- a/gix-traverse/src/commit/mod.rs +++ b/gix-traverse/src/commit/mod.rs @@ -5,6 +5,9 @@ use smallvec::SmallVec; pub mod ancestors; pub use ancestors::{Ancestors, Sorting}; +// Topological traversal +pub mod topo; + /// Specify how to handle commit parents during traversal. #[derive(Default, Copy, Clone)] pub enum Parents { diff --git a/gix-traverse/src/commit/topo.rs b/gix-traverse/src/commit/topo.rs new file mode 100644 index 00000000000..6954be2e955 --- /dev/null +++ b/gix-traverse/src/commit/topo.rs @@ -0,0 +1,611 @@ +//! Topological commit traversal +//! +//! Example: +//! ``` +//! let repo = gix::discover(".").uwnrap(); +//! let spec = repo.rev_parse_single("HEAD").unwrap(); +//! let walk = Builder::from_specs(&repo.objects, std::iter::once(spec)).unwrap(); +//! for commit_info in walk { +//! println!("{}", commit_info.id); +//! } +//! ``` + +use gix_hash::{oid, ObjectId}; +use gix_revwalk::{graph::IdMap, PriorityQueue}; + +use bitflags::bitflags; + +use smallvec::SmallVec; + +use super::{find, Either, Info, Parents}; + +#[derive(thiserror::Error, Debug)] +#[allow(missing_docs)] +/// The errors that can occur during creation and iteration. +pub enum Error { + #[error("Calculated indegree missing")] + MissingIndegree, + #[error("Internal state not found")] + MissingState, + #[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 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 +#[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. + TopoOrder, +} + +// 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)] +enum Queue { + Date(PriorityQueue), + Topo(Vec<(i64, Info)>), +} + +impl Queue { + fn new(s: Sorting) -> Self { + match s { + Sorting::DateOrder => Self::Date(PriorityQueue::new()), + Sorting::TopoOrder => Self::Topo(vec![]), + } + } + + 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), + } + } + + 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)); + } + } +} + +type GenAndCommitTime = (u32, i64); + +/// Builder for [`Walk`] +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 [`Walk`] that walks the given repository, + /// 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, + } + } + + /// Create a new `Builder` for a [`Walk`] that walks the given repository + /// from an iterator of `Specs`, given by the [`Spec`s](gix_revision::Spec) + pub fn from_specs(find: Find, specs: impl IntoIterator) -> Self { + let mut tips = vec![]; + let mut ends = vec![]; + + for spec in specs { + use gix_revision::Spec as S; + match spec { + S::Include(i) => tips.push(i), + S::Exclude(e) => ends.push(e), + S::Range { from, to } => { + tips.push(to); + ends.push(from) + } + S::Merge { .. } => todo!(), + S::IncludeOnlyParents(_) => todo!(), + S::ExcludeParents(_) => todo!(), + } + } + + 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 [`Walk`] instance. Note that merely building an instance is + /// currently expensive. + pub fn build(self) -> Result, Error> { + let mut w = Walk { + 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: 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::MissingIndegree)?; + + if *i == 1 { + 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) + } +} + +/// A commit walker that walks in topographical order, like `git rev-list +/// --topo-order` or `--date-order` depending on the chosen [`Sorting`] +pub struct Walk { + commit_graph: Option, + find: Find, + predicate: Predicate, + indegrees: IdMap, + states: IdMap, + explore_queue: PriorityQueue, + indegree_queue: PriorityQueue, + topo_queue: Queue, + parents: Parents, + min_gen: u32, + buf: Vec, +} + +impl Walk +where + Find: gix_object::Find, +{ + 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::MissingState)?; + + 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::MissingState)?; + + 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::MissingState)?; + + 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::MissingIndegree)?; + + *i -= 1; + + if *i == 1 { + 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::MissingState)?; + + 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.into(); + + 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, 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 + 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 id = self.topo_queue.pop()?; + + let i = match self.indegrees.get_mut(&id.id) { + Some(i) => i, + None => { + return Some(Err(Error::MissingIndegree)); + } + }; + + *i = 0; + + match self.expand_topo_walk(&id.id) { + Ok(_) => (), + Err(e) => { + return Some(Err(e)); + } + }; + + Some(Ok(id)) + } +} + +impl Iterator for Walk +where + Find: gix_object::Find, + Predicate: FnMut(&oid) -> bool, +{ + type Item = Result; + + fn next(&mut self) -> Option { + match self.pop_commit()? { + Ok(id) => { + if (self.predicate)(&id.id) { + Some(Ok(id)) + } else { + self.next() + } + } + Err(e) => 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) +} + +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/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 0000000000000000000000000000000000000000..3b33cd1d1a7bac6b189112c4620e05d1d8c5ff02 GIT binary patch literal 12364 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 literal 0 HcmV?d00001 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..dcfb1c35148 --- /dev/null +++ b/gix-traverse/tests/fixtures/make_repo_for_topo.sh @@ -0,0 +1,60 @@ +#!/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 +} + +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 diff --git a/gix-traverse/tests/topo/mod.rs b/gix-traverse/tests/topo/mod.rs new file mode 100644 index 00000000000..cd593678f16 --- /dev/null +++ b/gix-traverse/tests/topo/mod.rs @@ -0,0 +1,359 @@ +mod walk { + use gix_hash::{oid, ObjectId}; + use gix_traverse::commit::{topo, Parents}; + + use crate::hex_to_id; + + struct TraversalAssertion<'a> { + init_script: &'a str, + repo_name: &'a str, + tips: &'a [&'a str], + ends: &'a [&'a str], + expected: &'a [&'a str], + mode: Parents, + sorting: topo::Sorting, + } + + impl<'a> TraversalAssertion<'a> { + fn new(init_script: &'a str, tips: &'a [&'a str], ends: &'a [&'a str], expected: &'a [&'a str]) -> Self { + Self::new_at(init_script, "", 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, + 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 + } + } + + impl TraversalAssertion<'_> { + fn setup(&self) -> crate::Result<(gix_odb::Handle, Vec, 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 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") + } + + 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().cloned(), Some(ends.iter().cloned())) + .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 check(&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().cloned(), Some(ends.iter().cloned())) + .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(()) + } + } + + mod basic { + use gix_traverse::commit::topo; + + use super::TraversalAssertion; + + use crate::hex_to_id; + + #[test] + fn basic() -> crate::Result { + TraversalAssertion::new( + "make_repo_for_topo.sh", + &["62ed296d9986f50477e9f7b7e81cd0258939a43d"], + &[], + &[ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "3be0c4c793c634c8fd95054345d4935d10a0879a", + "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", + "302a5d0530ec688c241f32c2f2b61b964dd17bee", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + "f1cce1b5c7efcdfa106e95caa6c45a2cae48a481", + "945d8a360915631ad545e0cf04630d86d3d4eaa1", + "a863c02247a6c5ba32dff5224459f52aa7f77f7b", + "2f291881edfb0597493a52d26ea09dd7340ce507", + "9c46b8765703273feb10a2ebd810e70b8e2ca44a", + "fb3e21cf45b04b617011d2b30973f3e5ce60d0cd", + ], + ) + .with_sorting(topo::Sorting::TopoOrder) + .check() + } + + #[test] + fn one_end() -> crate::Result { + TraversalAssertion::new( + "make_repo_for_topo.sh", + &["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( + "make_repo_for_topo.sh", + &["f1cce1b5c7efcdfa106e95caa6c45a2cae48a481"], + &["eeab3243aad67bc838fc4425f759453bf0b47785"], + &[], + ) + .with_sorting(topo::Sorting::TopoOrder) + .check() + } + + #[test] + fn two_tips_two_ends() -> crate::Result { + TraversalAssertion::new( + "make_repo_for_topo.sh", + &[ + "d09384f312b03e4a1413160739805ff25e8fe99d", + "3be0c4c793c634c8fd95054345d4935d10a0879a", + ], + &[ + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + ], + &[ + "3be0c4c793c634c8fd95054345d4935d10a0879a", + "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", + "302a5d0530ec688c241f32c2f2b61b964dd17bee", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + ], + ) + .with_sorting(topo::Sorting::TopoOrder) + .check() + } + + #[test] + fn basic_with_dummy_predicate() -> crate::Result { + TraversalAssertion::new( + "make_repo_for_topo.sh", + &["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( + "make_repo_for_topo.sh", + &["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_first_parent() -> crate::Result { + TraversalAssertion::new( + "make_repo_for_topo.sh", + &["62ed296d9986f50477e9f7b7e81cd0258939a43d"], + &[], + &[ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + "f1cce1b5c7efcdfa106e95caa6c45a2cae48a481", + "945d8a360915631ad545e0cf04630d86d3d4eaa1", + "a863c02247a6c5ba32dff5224459f52aa7f77f7b", + "2f291881edfb0597493a52d26ea09dd7340ce507", + "9c46b8765703273feb10a2ebd810e70b8e2ca44a", + "fb3e21cf45b04b617011d2b30973f3e5ce60d0cd", + ], + ) + .with_parents(Parents::First) + .with_sorting(topo::Sorting::TopoOrder) + .check() + } + + #[test] + fn first_parent_with_end() -> crate::Result { + TraversalAssertion::new( + "make_repo_for_topo.sh", + &["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( + "make_repo_for_topo.sh", + &["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 basic_date_order() -> crate::Result { + TraversalAssertion::new( + "make_repo_for_topo.sh", + // 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/traverse.rs b/gix-traverse/tests/traverse.rs index 81ecb3be5b0..329b96c0837 100644 --- a/gix-traverse/tests/traverse.rs +++ b/gix-traverse/tests/traverse.rs @@ -5,4 +5,5 @@ fn hex_to_id(hex: &str) -> gix_hash::ObjectId { } mod commit; +mod topo; mod tree; From 7f6bee5452ee01638f89a0cec2d4ee2a6f0d0136 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 7 Apr 2024 09:12:54 +0200 Subject: [PATCH 04/10] thanks clippy --- gix-config/src/file/impls.rs | 2 +- gix-config/src/parse/comment.rs | 2 +- gix-config/src/parse/event.rs | 2 +- gix-config/src/parse/section/header.rs | 2 +- gix-pack/src/cache/delta/from_offsets.rs | 14 ++++++++------ gix/src/config/cache/util.rs | 14 -------------- gix/src/config/snapshot/credential_helpers.rs | 15 ++++++++++++++- gix/tests/repository/config/mod.rs | 1 - 8 files changed, 26 insertions(+), 26 deletions(-) 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-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/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/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)) From e0969c3cc928b31345c4f3a122466193697eb0fc Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 7 Apr 2024 09:39:26 +0200 Subject: [PATCH 05/10] fix!: simplify ancestor module.. We remove ability to reuse `State` as ultimately,this capability wasn't all that useful. --- gix-traverse/src/commit/ancestors.rs | 128 +++++++++++---------------- gix-traverse/src/commit/mod.rs | 12 ++- gix-traverse/tests/commit/mod.rs | 22 ++--- 3 files changed, 69 insertions(+), 93 deletions(-) diff --git a/gix-traverse/src/commit/ancestors.rs b/gix-traverse/src/commit/ancestors.rs index 9d61d80be4d..c72629ea075 100644 --- a/gix-traverse/src/commit/ancestors.rs +++ b/gix-traverse/src/commit/ancestors.rs @@ -1,14 +1,8 @@ +use gix_date::SecondsSinceUnixEpoch; +use gix_hash::ObjectId; +use gix_hashtable::HashSet; use smallvec::SmallVec; - -/// An iterator over the ancestors one or more starting commits -pub struct Ancestors { - objects: Find, - cache: Option, - predicate: Predicate, - state: StateMut, - parents: super::Parents, - sorting: Sorting, -} +use std::collections::VecDeque; /// Specify how to sort commits during traversal. /// @@ -58,46 +52,39 @@ pub enum Sorting { }, } +/// The error is part of the item returned by the [Ancestors](super::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(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 super::{ - super::{Either, Info, ParentIds, Parents}, - collect_parents, Ancestors, Sorting, + super::{Ancestors, Either, Info, ParentIds, Parents}, + collect_parents, Error, Sorting, State, }; - /// 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]>, - } - impl Default for State { fn default() -> Self { State { @@ -121,12 +108,11 @@ pub mod ancestors { } /// Builder - impl Ancestors + impl Ancestors 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 { @@ -135,7 +121,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; @@ -173,7 +159,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() @@ -182,41 +168,35 @@ pub mod ancestors { } } - /// Initialization - impl Ancestors bool, StateMut> + /// Lifecyle + impl Ancestors 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 Ancestors 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 @@ -224,13 +204,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) { @@ -250,27 +229,24 @@ pub mod ancestors { } } } + /// Access - impl Ancestors - where - StateMut: Borrow, - { - /// Return an iterator for accessing more of the current commits data. + impl Ancestors { + /// 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 Ancestors where Find: gix_object::Find, Predicate: FnMut(&oid) -> bool, - StateMut: BorrowMut, { type Item = Result; @@ -300,17 +276,16 @@ pub mod ancestors { } /// Utilities - impl Ancestors + impl Ancestors 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(); @@ -371,14 +346,13 @@ pub mod ancestors { } /// Utilities - impl Ancestors + impl Ancestors 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::super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) { @@ -430,8 +404,6 @@ pub mod ancestors { } } -pub use ancestors::{Error, State}; - fn collect_parents( dest: &mut SmallVec<[(gix_hash::ObjectId, gix_date::SecondsSinceUnixEpoch); 2]>, cache: Option<&gix_commitgraph::Graph>, diff --git a/gix-traverse/src/commit/mod.rs b/gix-traverse/src/commit/mod.rs index 406999fee65..e50f29cc556 100644 --- a/gix-traverse/src/commit/mod.rs +++ b/gix-traverse/src/commit/mod.rs @@ -1,9 +1,19 @@ use gix_object::FindExt; use smallvec::SmallVec; +/// An iterator over the ancestors one or more starting commits +pub struct Ancestors { + objects: Find, + cache: Option, + predicate: Predicate, + state: ancestors::State, + parents: Parents, + sorting: Sorting, +} + /// Simple ancestors traversal pub mod ancestors; -pub use ancestors::{Ancestors, Sorting}; +pub use ancestors::Sorting; // Topological traversal pub mod topo; diff --git a/gix-traverse/tests/commit/mod.rs b/gix-traverse/tests/commit/mod.rs index 6d9fac3e9c8..1debb1f84f5 100644 --- a/gix-traverse/tests/commit/mod.rs +++ b/gix-traverse/tests/commit/mod.rs @@ -64,17 +64,12 @@ mod ancestor { 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::, _>>()?; + let oids = commit::Ancestors::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); } @@ -85,7 +80,7 @@ mod ancestor { let (store, tips, expected) = self.setup()?; for use_commitgraph in [false, true] { - let oids = commit::Ancestors::new(tips.clone(), commit::ancestors::State::default(), &store) + let oids = commit::Ancestors::new(tips.clone(), &store) .sorting(self.sorting)? .parents(self.mode) .commit_graph(self.setup_commitgraph(store.store_ref(), use_commitgraph)) @@ -343,7 +338,7 @@ mod ancestor { /// 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 gix_traverse::commit::{Ancestors, Parents, Sorting}; use crate::{commit::ancestor::TraversalAssertion, hex_to_id}; @@ -398,7 +393,6 @@ mod ancestor { 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 { From 6154bf3a346d69f9749271d50e4f3aacdcbad4d0 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 7 Apr 2024 10:04:00 +0200 Subject: [PATCH 06/10] adapt to changes in `gix-traverse` --- gitoxide-core/src/pack/create.rs | 2 +- gix-diff/tests/tree/mod.rs | 2 +- .../pack/data/output/count_and_entries.rs | 2 +- gix/src/ext/object_id.rs | 6 +- gix/src/revision/walk.rs | 59 +++++++++---------- 5 files changed, 33 insertions(+), 38 deletions(-) diff --git a/gitoxide-core/src/pack/create.rs b/gitoxide-core/src/pack/create.rs index f8141e357d6..1229ddd2761 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::Ancestors::new(tips, handle.clone()) .map(|res| res.map_err(|err| Box::new(err) as Box<_>).map(|c| c.id)) .inspect(move |_| progress.inc()), ); diff --git a/gix-diff/tests/tree/mod.rs b/gix-diff/tests/tree/mod.rs index 7df8f64c7c9..ace13c36c99 100644 --- a/gix-diff/tests/tree/mod.rs +++ b/gix-diff/tests/tree/mod.rs @@ -133,7 +133,7 @@ mod changes { let mut buf = Vec::new(); let head = head_of(db); - commit::Ancestors::new(Some(head), commit::ancestors::State::default(), &db) + commit::Ancestors::new(Some(head), &db) .collect::, _>>() .expect("valid iteration") .into_iter() 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..c5e7b960d98 100644 --- a/gix-pack/tests/pack/data/output/count_and_entries.rs +++ b/gix-pack/tests/pack/data/output/count_and_entries.rs @@ -241,7 +241,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 = commit::Ancestors::new(Some(head), db.clone()) .map(Result::unwrap) .map(|c| c.id) .collect::>(); diff --git a/gix/src/ext/object_id.rs b/gix/src/ext/object_id.rs index d4d9467664b..018c0ab032b 100644 --- a/gix/src/ext/object_id.rs +++ b/gix/src/ext/object_id.rs @@ -1,9 +1,9 @@ use gix_hash::ObjectId; -use gix_traverse::commit::{ancestors, Ancestors}; +use gix_traverse::commit::Ancestors; pub trait Sealed {} -pub type AncestorsIter = Ancestors bool, ancestors::State>; +pub type AncestorsIter = Ancestors bool>; /// An extension trait to add functionality to [`ObjectId`]s. pub trait ObjectIdExt: Sealed { @@ -23,7 +23,7 @@ impl ObjectIdExt for ObjectId { where Find: gix_object::Find, { - Ancestors::new(Some(self), ancestors::State::default(), find) + Ancestors::new(Some(self), find) } fn attach(self, repo: &crate::Repository) -> crate::Id<'_> { diff --git a/gix/src/revision/walk.rs b/gix/src/revision/walk.rs index 19d15d569ab..a089733a479 100644 --- a/gix/src/revision/walk.rs +++ b/gix/src/revision/walk.rs @@ -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::Ancestors::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( From ee13bdb94e574a35c43cc6b6353f39e6c2d5c786 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 7 Apr 2024 10:49:17 +0200 Subject: [PATCH 07/10] refactor - adjust module structure - remove unused method with `todo!()` - no recursion in `next()` - baseline comparisons --- Cargo.lock | 1 - gix-traverse/Cargo.toml | 1 - gix-traverse/src/commit/topo.rs | 611 ------------------ gix-traverse/src/commit/topo/init.rs | 174 +++++ gix-traverse/src/commit/topo/iter.rs | 312 +++++++++ gix-traverse/src/commit/topo/mod.rs | 79 +++ gix-traverse/tests/commit/ancestor.rs | 422 ++++++++++++ gix-traverse/tests/commit/mod.rs | 426 +----------- gix-traverse/tests/commit/topo.rs | 372 +++++++++++ .../make_repo_for_topo.tar.xz | Bin 12364 -> 12552 bytes .../tests/fixtures/make_repo_for_topo.sh | 6 + gix-traverse/tests/topo/mod.rs | 359 ---------- gix-traverse/tests/traverse.rs | 1 - 13 files changed, 1367 insertions(+), 1397 deletions(-) delete mode 100644 gix-traverse/src/commit/topo.rs create mode 100644 gix-traverse/src/commit/topo/init.rs create mode 100644 gix-traverse/src/commit/topo/iter.rs create mode 100644 gix-traverse/src/commit/topo/mod.rs create mode 100644 gix-traverse/tests/commit/ancestor.rs create mode 100644 gix-traverse/tests/commit/topo.rs delete mode 100644 gix-traverse/tests/topo/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 887b13ecbf0..cae30bec939 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2684,7 +2684,6 @@ dependencies = [ "gix-hash 0.14.2", "gix-hashtable 0.5.2", "gix-object 0.42.1", - "gix-revision", "gix-revwalk 0.13.0", "smallvec", "thiserror", diff --git a/gix-traverse/Cargo.toml b/gix-traverse/Cargo.toml index 6cd63dad837..cc505637b37 100644 --- a/gix-traverse/Cargo.toml +++ b/gix-traverse/Cargo.toml @@ -20,7 +20,6 @@ gix-date = { version = "^0.8.5", path = "../gix-date" } gix-hashtable = { version = "^0.5.2", path = "../gix-hashtable" } gix-revwalk = { version = "^0.13.0", path = "../gix-revwalk" } gix-commitgraph = { version = "^0.24.2", path = "../gix-commitgraph" } -gix-revision = { version = "^0.27.0", path = "../gix-revision" } smallvec = "1.10.0" thiserror = "1.0.32" bitflags = "2" diff --git a/gix-traverse/src/commit/topo.rs b/gix-traverse/src/commit/topo.rs deleted file mode 100644 index 6954be2e955..00000000000 --- a/gix-traverse/src/commit/topo.rs +++ /dev/null @@ -1,611 +0,0 @@ -//! Topological commit traversal -//! -//! Example: -//! ``` -//! let repo = gix::discover(".").uwnrap(); -//! let spec = repo.rev_parse_single("HEAD").unwrap(); -//! let walk = Builder::from_specs(&repo.objects, std::iter::once(spec)).unwrap(); -//! for commit_info in walk { -//! println!("{}", commit_info.id); -//! } -//! ``` - -use gix_hash::{oid, ObjectId}; -use gix_revwalk::{graph::IdMap, PriorityQueue}; - -use bitflags::bitflags; - -use smallvec::SmallVec; - -use super::{find, Either, Info, Parents}; - -#[derive(thiserror::Error, Debug)] -#[allow(missing_docs)] -/// The errors that can occur during creation and iteration. -pub enum Error { - #[error("Calculated indegree missing")] - MissingIndegree, - #[error("Internal state not found")] - MissingState, - #[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 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 -#[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. - TopoOrder, -} - -// 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)] -enum Queue { - Date(PriorityQueue), - Topo(Vec<(i64, Info)>), -} - -impl Queue { - fn new(s: Sorting) -> Self { - match s { - Sorting::DateOrder => Self::Date(PriorityQueue::new()), - Sorting::TopoOrder => Self::Topo(vec![]), - } - } - - 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), - } - } - - 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)); - } - } -} - -type GenAndCommitTime = (u32, i64); - -/// Builder for [`Walk`] -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 [`Walk`] that walks the given repository, - /// 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, - } - } - - /// Create a new `Builder` for a [`Walk`] that walks the given repository - /// from an iterator of `Specs`, given by the [`Spec`s](gix_revision::Spec) - pub fn from_specs(find: Find, specs: impl IntoIterator) -> Self { - let mut tips = vec![]; - let mut ends = vec![]; - - for spec in specs { - use gix_revision::Spec as S; - match spec { - S::Include(i) => tips.push(i), - S::Exclude(e) => ends.push(e), - S::Range { from, to } => { - tips.push(to); - ends.push(from) - } - S::Merge { .. } => todo!(), - S::IncludeOnlyParents(_) => todo!(), - S::ExcludeParents(_) => todo!(), - } - } - - 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 [`Walk`] instance. Note that merely building an instance is - /// currently expensive. - pub fn build(self) -> Result, Error> { - let mut w = Walk { - 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: 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::MissingIndegree)?; - - if *i == 1 { - 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) - } -} - -/// A commit walker that walks in topographical order, like `git rev-list -/// --topo-order` or `--date-order` depending on the chosen [`Sorting`] -pub struct Walk { - commit_graph: Option, - find: Find, - predicate: Predicate, - indegrees: IdMap, - states: IdMap, - explore_queue: PriorityQueue, - indegree_queue: PriorityQueue, - topo_queue: Queue, - parents: Parents, - min_gen: u32, - buf: Vec, -} - -impl Walk -where - Find: gix_object::Find, -{ - 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::MissingState)?; - - 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::MissingState)?; - - 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::MissingState)?; - - 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::MissingIndegree)?; - - *i -= 1; - - if *i == 1 { - 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::MissingState)?; - - 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.into(); - - 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, 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 - 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 id = self.topo_queue.pop()?; - - let i = match self.indegrees.get_mut(&id.id) { - Some(i) => i, - None => { - return Some(Err(Error::MissingIndegree)); - } - }; - - *i = 0; - - match self.expand_topo_walk(&id.id) { - Ok(_) => (), - Err(e) => { - return Some(Err(e)); - } - }; - - Some(Ok(id)) - } -} - -impl Iterator for Walk -where - Find: gix_object::Find, - Predicate: FnMut(&oid) -> bool, -{ - type Item = Result; - - fn next(&mut self) -> Option { - match self.pop_commit()? { - Ok(id) => { - if (self.predicate)(&id.id) { - Some(Ok(id)) - } else { - self.next() - } - } - Err(e) => 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) -} - -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/init.rs b/gix-traverse/src/commit/topo/init.rs new file mode 100644 index 00000000000..4506832a6bb --- /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, Walk, WalkFlags}; +use crate::commit::{find, Info, Parents}; +use gix_hash::{oid, ObjectId}; +use gix_revwalk::graph::IdMap; +use gix_revwalk::PriorityQueue; + +/// Builder for [`Walk`]. +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 [`Walk`] 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 [`Walk`] instance. + /// + /// Note that merely building an instance is currently expensive. + pub fn build(self) -> Result, Error> { + let mut w = Walk { + 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..121a31860fa --- /dev/null +++ b/gix-traverse/src/commit/topo/iter.rs @@ -0,0 +1,312 @@ +use crate::commit::topo::{Error, Sorting, Walk, WalkFlags}; +use crate::commit::{find, Either, Info, Parents}; +use gix_hash::{oid, ObjectId}; +use gix_revwalk::PriorityQueue; +use smallvec::SmallVec; + +pub(super) 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(super) 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 Walk +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 Walk +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..0c146ab92d4 --- /dev/null +++ b/gix-traverse/src/commit/topo/mod.rs @@ -0,0 +1,79 @@ +//! Topological commit traversal, similar to `git log --topo-order`. + +use gix_hash::ObjectId; +use gix_revwalk::{graph::IdMap, PriorityQueue}; + +use bitflags::bitflags; + +use super::Parents; + +/// A commit walker that walks in topographical order, like `git rev-list +/// --topo-order` or `--date-order` depending on the chosen [`Sorting`]. +pub struct Walk { + commit_graph: Option, + find: Find, + predicate: Predicate, + indegrees: IdMap, + states: IdMap, + explore_queue: PriorityQueue, + indegree_queue: PriorityQueue, + topo_queue: iter::Queue, + parents: Parents, + min_gen: u32, + buf: Vec, +} + +/// 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)] + 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 +#[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. + TopoOrder, +} + +mod init; +pub use init::Builder; + +mod iter; diff --git a/gix-traverse/tests/commit/ancestor.rs b/gix-traverse/tests/commit/ancestor.rs new file mode 100644 index 00000000000..6a9b2faa6c3 --- /dev/null +++ b/gix-traverse/tests/commit/ancestor.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::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(), &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(), &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, 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 */)), + &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/mod.rs b/gix-traverse/tests/commit/mod.rs index 1debb1f84f5..5fda4796282 100644 --- a/gix-traverse/tests/commit/mod.rs +++ b/gix-traverse/tests/commit/mod.rs @@ -1,424 +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(), &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(), &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, 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 */)), - &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 ancestor; +mod topo; 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 index 3b33cd1d1a7bac6b189112c4620e05d1d8c5ff02..12a8797c09727fa56608b6da1e0e8fa051d1a93d 100644 GIT binary patch literal 12552 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 diff --git a/gix-traverse/tests/fixtures/make_repo_for_topo.sh b/gix-traverse/tests/fixtures/make_repo_for_topo.sh index dcfb1c35148..6550da2e2c0 100755 --- a/gix-traverse/tests/fixtures/make_repo_for_topo.sh +++ b/gix-traverse/tests/fixtures/make_repo_for_topo.sh @@ -25,6 +25,11 @@ function optimize() { 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 @@ -58,3 +63,4 @@ git merge branch1 -m merge commit c12 optimize +collect_baselines diff --git a/gix-traverse/tests/topo/mod.rs b/gix-traverse/tests/topo/mod.rs deleted file mode 100644 index cd593678f16..00000000000 --- a/gix-traverse/tests/topo/mod.rs +++ /dev/null @@ -1,359 +0,0 @@ -mod walk { - use gix_hash::{oid, ObjectId}; - use gix_traverse::commit::{topo, Parents}; - - use crate::hex_to_id; - - struct TraversalAssertion<'a> { - init_script: &'a str, - repo_name: &'a str, - tips: &'a [&'a str], - ends: &'a [&'a str], - expected: &'a [&'a str], - mode: Parents, - sorting: topo::Sorting, - } - - impl<'a> TraversalAssertion<'a> { - fn new(init_script: &'a str, tips: &'a [&'a str], ends: &'a [&'a str], expected: &'a [&'a str]) -> Self { - Self::new_at(init_script, "", 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, - 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 - } - } - - impl TraversalAssertion<'_> { - fn setup(&self) -> crate::Result<(gix_odb::Handle, Vec, 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 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") - } - - 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().cloned(), Some(ends.iter().cloned())) - .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 check(&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().cloned(), Some(ends.iter().cloned())) - .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(()) - } - } - - mod basic { - use gix_traverse::commit::topo; - - use super::TraversalAssertion; - - use crate::hex_to_id; - - #[test] - fn basic() -> crate::Result { - TraversalAssertion::new( - "make_repo_for_topo.sh", - &["62ed296d9986f50477e9f7b7e81cd0258939a43d"], - &[], - &[ - "62ed296d9986f50477e9f7b7e81cd0258939a43d", - "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", - "3be0c4c793c634c8fd95054345d4935d10a0879a", - "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", - "302a5d0530ec688c241f32c2f2b61b964dd17bee", - "d09384f312b03e4a1413160739805ff25e8fe99d", - "22fbc169eeca3c9678fc7028aa80fad5ef49019f", - "eeab3243aad67bc838fc4425f759453bf0b47785", - "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", - "33eb18340e4eaae3e3dcf80222b02f161cd3f966", - "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", - "f1cce1b5c7efcdfa106e95caa6c45a2cae48a481", - "945d8a360915631ad545e0cf04630d86d3d4eaa1", - "a863c02247a6c5ba32dff5224459f52aa7f77f7b", - "2f291881edfb0597493a52d26ea09dd7340ce507", - "9c46b8765703273feb10a2ebd810e70b8e2ca44a", - "fb3e21cf45b04b617011d2b30973f3e5ce60d0cd", - ], - ) - .with_sorting(topo::Sorting::TopoOrder) - .check() - } - - #[test] - fn one_end() -> crate::Result { - TraversalAssertion::new( - "make_repo_for_topo.sh", - &["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( - "make_repo_for_topo.sh", - &["f1cce1b5c7efcdfa106e95caa6c45a2cae48a481"], - &["eeab3243aad67bc838fc4425f759453bf0b47785"], - &[], - ) - .with_sorting(topo::Sorting::TopoOrder) - .check() - } - - #[test] - fn two_tips_two_ends() -> crate::Result { - TraversalAssertion::new( - "make_repo_for_topo.sh", - &[ - "d09384f312b03e4a1413160739805ff25e8fe99d", - "3be0c4c793c634c8fd95054345d4935d10a0879a", - ], - &[ - "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", - "22fbc169eeca3c9678fc7028aa80fad5ef49019f", - ], - &[ - "3be0c4c793c634c8fd95054345d4935d10a0879a", - "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", - "302a5d0530ec688c241f32c2f2b61b964dd17bee", - "d09384f312b03e4a1413160739805ff25e8fe99d", - "eeab3243aad67bc838fc4425f759453bf0b47785", - "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", - "33eb18340e4eaae3e3dcf80222b02f161cd3f966", - ], - ) - .with_sorting(topo::Sorting::TopoOrder) - .check() - } - - #[test] - fn basic_with_dummy_predicate() -> crate::Result { - TraversalAssertion::new( - "make_repo_for_topo.sh", - &["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( - "make_repo_for_topo.sh", - &["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_first_parent() -> crate::Result { - TraversalAssertion::new( - "make_repo_for_topo.sh", - &["62ed296d9986f50477e9f7b7e81cd0258939a43d"], - &[], - &[ - "62ed296d9986f50477e9f7b7e81cd0258939a43d", - "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", - "d09384f312b03e4a1413160739805ff25e8fe99d", - "eeab3243aad67bc838fc4425f759453bf0b47785", - "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", - "33eb18340e4eaae3e3dcf80222b02f161cd3f966", - "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", - "f1cce1b5c7efcdfa106e95caa6c45a2cae48a481", - "945d8a360915631ad545e0cf04630d86d3d4eaa1", - "a863c02247a6c5ba32dff5224459f52aa7f77f7b", - "2f291881edfb0597493a52d26ea09dd7340ce507", - "9c46b8765703273feb10a2ebd810e70b8e2ca44a", - "fb3e21cf45b04b617011d2b30973f3e5ce60d0cd", - ], - ) - .with_parents(Parents::First) - .with_sorting(topo::Sorting::TopoOrder) - .check() - } - - #[test] - fn first_parent_with_end() -> crate::Result { - TraversalAssertion::new( - "make_repo_for_topo.sh", - &["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( - "make_repo_for_topo.sh", - &["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 basic_date_order() -> crate::Result { - TraversalAssertion::new( - "make_repo_for_topo.sh", - // 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/traverse.rs b/gix-traverse/tests/traverse.rs index 329b96c0837..81ecb3be5b0 100644 --- a/gix-traverse/tests/traverse.rs +++ b/gix-traverse/tests/traverse.rs @@ -5,5 +5,4 @@ fn hex_to_id(hex: &str) -> gix_hash::ObjectId { } mod commit; -mod topo; mod tree; From 3f938faf9e6323b8056ee286f299747e15e6a0d3 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 7 Apr 2024 16:09:25 +0200 Subject: [PATCH 08/10] fix!: adjust the structure of `commit::ancestors` to be more consistent with other plumbing crates. This makes sure that each time is reached by only a single path. --- gix-traverse/src/commit/ancestors.rs | 52 ++-------------------------- gix-traverse/src/commit/mod.rs | 50 ++++++++++++++++++++++++-- gix-traverse/src/commit/topo/mod.rs | 16 +++++++-- 3 files changed, 64 insertions(+), 54 deletions(-) diff --git a/gix-traverse/src/commit/ancestors.rs b/gix-traverse/src/commit/ancestors.rs index c72629ea075..51b647714e4 100644 --- a/gix-traverse/src/commit/ancestors.rs +++ b/gix-traverse/src/commit/ancestors.rs @@ -4,54 +4,6 @@ use gix_hashtable::HashSet; use smallvec::SmallVec; use std::collections::VecDeque; -/// Specify how to sort commits during traversal. -/// -/// ### 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(Default, Debug, Copy, Clone)] -pub enum Sorting { - /// Commits are sorted as they are mentioned in the commit graph. - /// - /// In the *sample history* the order would be `8, 6, 7, 5, 4, 3, 2, 1` - /// - /// ### Note - /// - /// This is not to be confused with `git log/rev-list --topo-order`, which is notably different from - /// as it avoids overlapping branches. - #[default] - BreadthFirst, - /// Commits are sorted by their commit time in descending order, that is newest first. - /// - /// The sorting applies to all currently queued commit ids and thus is full. - /// - /// In the *sample history* the order would be `8, 7, 6, 5, 4, 3, 2, 1` - /// - /// # Performance - /// - /// This mode benefits greatly from having an object_cache in `find()` - /// to avoid having to lookup each commit twice. - ByCommitTimeNewestFirst, - /// This sorting is similar to `ByCommitTimeNewestFirst`, but adds a cutoff to not return commits older than - /// a given time, stopping the iteration once no younger commits is queued to be traversed. - /// - /// As the query is usually repeated with different cutoff dates, this search mode benefits greatly from an object cache. - /// - /// In the *sample history* and a cut-off date of 4, the returned list of commits would be `8, 7, 6, 4` - ByCommitTimeNewestFirstCutoffOlderThan { - /// The amount of seconds since unix epoch, the same value obtained by any `gix_date::Time` structure and the way git counts time. - seconds: gix_date::SecondsSinceUnixEpoch, - }, -} - /// The error is part of the item returned by the [Ancestors](super::Ancestors) iterator. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] @@ -81,8 +33,8 @@ mod init { use gix_object::{CommitRefIter, FindExt}; use super::{ - super::{Ancestors, Either, Info, ParentIds, Parents}, - collect_parents, Error, Sorting, State, + super::{Ancestors, Either, Info, ParentIds, Parents, Sorting}, + collect_parents, Error, State, }; impl Default for State { diff --git a/gix-traverse/src/commit/mod.rs b/gix-traverse/src/commit/mod.rs index e50f29cc556..f78881614a8 100644 --- a/gix-traverse/src/commit/mod.rs +++ b/gix-traverse/src/commit/mod.rs @@ -1,7 +1,7 @@ use gix_object::FindExt; use smallvec::SmallVec; -/// An iterator over the ancestors one or more starting commits +/// A fast iterator over the ancestors of one or more starting commits. pub struct Ancestors { objects: Find, cache: Option, @@ -11,9 +11,55 @@ pub struct Ancestors { sorting: Sorting, } +/// Specify how to sort commits during the [ancestor](Ancestors) traversal. +/// +/// ### 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(Default, Debug, Copy, Clone)] +pub enum Sorting { + /// Commits are sorted as they are mentioned in the commit graph. + /// + /// In the *sample history* the order would be `8, 6, 7, 5, 4, 3, 2, 1` + /// + /// ### Note + /// + /// This is not to be confused with `git log/rev-list --topo-order`, which is notably different from + /// as it avoids overlapping branches. + #[default] + BreadthFirst, + /// Commits are sorted by their commit time in descending order, that is newest first. + /// + /// The sorting applies to all currently queued commit ids and thus is full. + /// + /// In the *sample history* the order would be `8, 7, 6, 5, 4, 3, 2, 1` + /// + /// # Performance + /// + /// This mode benefits greatly from having an object_cache in `find()` + /// to avoid having to lookup each commit twice. + ByCommitTimeNewestFirst, + /// This sorting is similar to `ByCommitTimeNewestFirst`, but adds a cutoff to not return commits older than + /// a given time, stopping the iteration once no younger commits is queued to be traversed. + /// + /// As the query is usually repeated with different cutoff dates, this search mode benefits greatly from an object cache. + /// + /// In the *sample history* and a cut-off date of 4, the returned list of commits would be `8, 7, 6, 4` + ByCommitTimeNewestFirstCutoffOlderThan { + /// The amount of seconds since unix epoch, the same value obtained by any `gix_date::Time` structure and the way git counts time. + seconds: gix_date::SecondsSinceUnixEpoch, + }, +} + /// Simple ancestors traversal pub mod ancestors; -pub use ancestors::Sorting; // Topological traversal pub mod topo; diff --git a/gix-traverse/src/commit/topo/mod.rs b/gix-traverse/src/commit/topo/mod.rs index 0c146ab92d4..7d7aa61932e 100644 --- a/gix-traverse/src/commit/topo/mod.rs +++ b/gix-traverse/src/commit/topo/mod.rs @@ -60,16 +60,28 @@ bitflags! { } } -/// Sorting to use for the topological walk +/// 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, } From 2a9c178326b7f13ba6bc1f89fc2b9d9facbecf48 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 7 Apr 2024 20:15:26 +0200 Subject: [PATCH 09/10] fix!: Make `topo` more similar to `Ancestors`, but also rename `Ancestors` to `Simple` --- gitoxide-core/src/hours/mod.rs | 2 +- gitoxide-core/src/pack/create.rs | 4 +- gitoxide-core/src/query/engine/update.rs | 2 +- gix-diff/tests/tree/mod.rs | 2 +- .../pack/data/output/count_and_entries.rs | 2 +- gix-traverse/src/commit/mod.rs | 82 +++++++------------ .../src/commit/{ancestors.rs => simple.rs} | 65 +++++++++++++-- gix-traverse/src/commit/topo/init.rs | 14 ++-- gix-traverse/src/commit/topo/iter.rs | 12 +-- gix-traverse/src/commit/topo/mod.rs | 27 +----- gix-traverse/src/lib.rs | 1 - gix-traverse/tests/commit/mod.rs | 2 +- .../tests/commit/{ancestor.rs => simple.rs} | 26 +++--- gix/src/ext/object_id.rs | 6 +- gix/src/revision/spec/parse/types.rs | 2 +- gix/src/revision/walk.rs | 11 ++- 16 files changed, 130 insertions(+), 130 deletions(-) rename gix-traverse/src/commit/{ancestors.rs => simple.rs} (85%) rename gix-traverse/tests/commit/{ancestor.rs => simple.rs} (95%) 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 1229ddd2761..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, 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/gix-diff/tests/tree/mod.rs b/gix-diff/tests/tree/mod.rs index ace13c36c99..bd57c5fc8dd 100644 --- a/gix-diff/tests/tree/mod.rs +++ b/gix-diff/tests/tree/mod.rs @@ -133,7 +133,7 @@ mod changes { let mut buf = Vec::new(); let head = head_of(db); - commit::Ancestors::new(Some(head), &db) + commit::Simple::new(Some(head), &db) .collect::, _>>() .expect("valid iteration") .into_iter() 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 c5e7b960d98..f357f894798 100644 --- a/gix-pack/tests/pack/data/output/count_and_entries.rs +++ b/gix-pack/tests/pack/data/output/count_and_entries.rs @@ -241,7 +241,7 @@ fn traversals() -> crate::Result { .copied() { let head = hex_to_id("dfcb5e39ac6eb30179808bbab721e8a28ce1b52e"); - let mut commits = commit::Ancestors::new(Some(head), db.clone()) + let mut commits = commit::Simple::new(Some(head), db.clone()) .map(Result::unwrap) .map(|c| c.id) .collect::>(); diff --git a/gix-traverse/src/commit/mod.rs b/gix-traverse/src/commit/mod.rs index f78881614a8..26e9287ae70 100644 --- a/gix-traverse/src/commit/mod.rs +++ b/gix-traverse/src/commit/mod.rs @@ -1,67 +1,43 @@ +//! 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 Ancestors { +pub struct Simple { objects: Find, cache: Option, predicate: Predicate, - state: ancestors::State, + state: simple::State, parents: Parents, - sorting: Sorting, + sorting: simple::Sorting, } -/// Specify how to sort commits during the [ancestor](Ancestors) traversal. -/// -/// ### 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*). +/// 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`]. /// -/// ```text -/// ---1----2----4----7 <- second parent of 8 -/// \ \ -/// 3----5----6----8--- -/// ``` -#[derive(Default, Debug, Copy, Clone)] -pub enum Sorting { - /// Commits are sorted as they are mentioned in the commit graph. - /// - /// In the *sample history* the order would be `8, 6, 7, 5, 4, 3, 2, 1` - /// - /// ### Note - /// - /// This is not to be confused with `git log/rev-list --topo-order`, which is notably different from - /// as it avoids overlapping branches. - #[default] - BreadthFirst, - /// Commits are sorted by their commit time in descending order, that is newest first. - /// - /// The sorting applies to all currently queued commit ids and thus is full. - /// - /// In the *sample history* the order would be `8, 7, 6, 5, 4, 3, 2, 1` - /// - /// # Performance - /// - /// This mode benefits greatly from having an object_cache in `find()` - /// to avoid having to lookup each commit twice. - ByCommitTimeNewestFirst, - /// This sorting is similar to `ByCommitTimeNewestFirst`, but adds a cutoff to not return commits older than - /// a given time, stopping the iteration once no younger commits is queued to be traversed. - /// - /// As the query is usually repeated with different cutoff dates, this search mode benefits greatly from an object cache. - /// - /// In the *sample history* and a cut-off date of 4, the returned list of commits would be `8, 7, 6, 4` - ByCommitTimeNewestFirstCutoffOlderThan { - /// The amount of seconds since unix epoch, the same value obtained by any `gix_date::Time` structure and the way git counts time. - seconds: gix_date::SecondsSinceUnixEpoch, - }, +/// 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, } -/// Simple ancestors traversal -pub mod ancestors; - -// Topological traversal pub mod topo; /// Specify how to handle commit parents during traversal. @@ -86,8 +62,8 @@ pub struct Info { 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. + /// 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, } diff --git a/gix-traverse/src/commit/ancestors.rs b/gix-traverse/src/commit/simple.rs similarity index 85% rename from gix-traverse/src/commit/ancestors.rs rename to gix-traverse/src/commit/simple.rs index 51b647714e4..a4a3ff391c8 100644 --- a/gix-traverse/src/commit/ancestors.rs +++ b/gix-traverse/src/commit/simple.rs @@ -4,7 +4,54 @@ use gix_hashtable::HashSet; use smallvec::SmallVec; use std::collections::VecDeque; -/// The error is part of the item returned by the [Ancestors](super::Ancestors) iterator. +/// Specify how to sort commits during a [simple](super::Simple) traversal. +/// +/// ### 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(Default, Debug, Copy, Clone)] +pub enum Sorting { + /// Commits are sorted as they are mentioned in the commit graph. + /// + /// In the *sample history* the order would be `8, 6, 7, 5, 4, 3, 2, 1` + /// + /// ### Note + /// + /// This is not to be confused with `git log/rev-list --topo-order`, which is notably different from + /// as it avoids overlapping branches. + #[default] + BreadthFirst, + /// Commits are sorted by their commit time in descending order, that is newest first. + /// + /// The sorting applies to all currently queued commit ids and thus is full. + /// + /// In the *sample history* the order would be `8, 7, 6, 5, 4, 3, 2, 1` + /// + /// # Performance + /// + /// This mode benefits greatly from having an object_cache in `find()` + /// to avoid having to lookup each commit twice. + ByCommitTimeNewestFirst, + /// This sorting is similar to `ByCommitTimeNewestFirst`, but adds a cutoff to not return commits older than + /// a given time, stopping the iteration once no younger commits is queued to be traversed. + /// + /// As the query is usually repeated with different cutoff dates, this search mode benefits greatly from an object cache. + /// + /// In the *sample history* and a cut-off date of 4, the returned list of commits would be `8, 7, 6, 4` + ByCommitTimeNewestFirstCutoffOlderThan { + /// The amount of seconds since unix epoch, the same value obtained by any `gix_date::Time` structure and the way git counts time. + seconds: gix_date::SecondsSinceUnixEpoch, + }, +} + +/// The error is part of the item returned by the [Ancestors](super::Simple) iterator. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { @@ -33,7 +80,7 @@ mod init { use gix_object::{CommitRefIter, FindExt}; use super::{ - super::{Ancestors, Either, Info, ParentIds, Parents, Sorting}, + super::{simple::Sorting, Either, Info, ParentIds, Parents, Simple}, collect_parents, Error, State, }; @@ -60,7 +107,7 @@ mod init { } /// Builder - impl Ancestors + impl Simple where Find: gix_object::Find, { @@ -121,7 +168,7 @@ mod init { } /// Lifecyle - impl Ancestors bool> + impl Simple bool> where Find: gix_object::Find, { @@ -139,7 +186,7 @@ mod init { } /// Lifecyle - impl Ancestors + impl Simple where Find: gix_object::Find, Predicate: FnMut(&oid) -> bool, @@ -183,7 +230,7 @@ mod init { } /// Access - impl Ancestors { + 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.buf) @@ -195,7 +242,7 @@ mod init { } } - impl Iterator for Ancestors + impl Iterator for Simple where Find: gix_object::Find, Predicate: FnMut(&oid) -> bool, @@ -228,7 +275,7 @@ mod init { } /// Utilities - impl Ancestors + impl Simple where Find: gix_object::Find, Predicate: FnMut(&oid) -> bool, @@ -298,7 +345,7 @@ mod init { } /// Utilities - impl Ancestors + impl Simple where Find: gix_object::Find, Predicate: FnMut(&oid) -> bool, diff --git a/gix-traverse/src/commit/topo/init.rs b/gix-traverse/src/commit/topo/init.rs index 4506832a6bb..42972d2b871 100644 --- a/gix-traverse/src/commit/topo/init.rs +++ b/gix-traverse/src/commit/topo/init.rs @@ -1,11 +1,11 @@ use crate::commit::topo::iter::gen_and_commit_time; -use crate::commit::topo::{Error, Sorting, Walk, WalkFlags}; -use crate::commit::{find, Info, Parents}; +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 [`Walk`]. +/// Builder for [`Topo`]. pub struct Builder { commit_graph: Option, find: Find, @@ -20,7 +20,7 @@ impl Builder bool> where Find: gix_object::Find, { - /// Create a new `Builder` for a [`Walk`] that reads commits from a repository with `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( @@ -87,11 +87,11 @@ where self } - /// Build a new [`Walk`] instance. + /// Build a new [`Topo`] instance. /// /// Note that merely building an instance is currently expensive. - pub fn build(self) -> Result, Error> { - let mut w = Walk { + pub fn build(self) -> Result, Error> { + let mut w = Topo { commit_graph: self.commit_graph, find: self.find, predicate: self.predicate, diff --git a/gix-traverse/src/commit/topo/iter.rs b/gix-traverse/src/commit/topo/iter.rs index 121a31860fa..09f38eb7e7a 100644 --- a/gix-traverse/src/commit/topo/iter.rs +++ b/gix-traverse/src/commit/topo/iter.rs @@ -1,17 +1,17 @@ -use crate::commit::topo::{Error, Sorting, Walk, WalkFlags}; -use crate::commit::{find, Either, Info, Parents}; +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(super) type GenAndCommitTime = (u32, i64); +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(super) enum Queue { +pub(in crate::commit) enum Queue { Date(PriorityQueue), Topo(Vec<(i64, Info)>), } @@ -45,7 +45,7 @@ impl Queue { } } -impl Walk +impl Topo where Find: gix_object::Find, { @@ -214,7 +214,7 @@ where } } -impl Iterator for Walk +impl Iterator for Topo where Find: gix_object::Find, Predicate: FnMut(&oid) -> bool, diff --git a/gix-traverse/src/commit/topo/mod.rs b/gix-traverse/src/commit/topo/mod.rs index 7d7aa61932e..6ae543c106c 100644 --- a/gix-traverse/src/commit/topo/mod.rs +++ b/gix-traverse/src/commit/topo/mod.rs @@ -1,28 +1,7 @@ -//! Topological commit traversal, similar to `git log --topo-order`. - -use gix_hash::ObjectId; -use gix_revwalk::{graph::IdMap, PriorityQueue}; +//! Topological commit traversal, similar to `git log --topo-order`, which keeps track of graph state. use bitflags::bitflags; -use super::Parents; - -/// A commit walker that walks in topographical order, like `git rev-list -/// --topo-order` or `--date-order` depending on the chosen [`Sorting`]. -pub struct Walk { - commit_graph: Option, - find: Find, - predicate: Predicate, - indegrees: IdMap, - states: IdMap, - explore_queue: PriorityQueue, - indegree_queue: PriorityQueue, - topo_queue: iter::Queue, - parents: Parents, - min_gen: u32, - buf: Vec, -} - /// The errors that can occur during creation and iteration. #[derive(thiserror::Error, Debug)] #[allow(missing_docs)] @@ -44,7 +23,7 @@ bitflags! { // NOTE: The names correspond to the names of the flags in revision.h #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - struct WalkFlags: u32 { + pub(super) struct WalkFlags: u32 { /// Commit has been seen const Seen = 0b000001; /// Commit has been processed by the Explore walk @@ -88,4 +67,4 @@ pub enum Sorting { mod init; pub use init::Builder; -mod iter; +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 5fda4796282..7aa327bfa53 100644 --- a/gix-traverse/tests/commit/mod.rs +++ b/gix-traverse/tests/commit/mod.rs @@ -1,2 +1,2 @@ -mod ancestor; +mod simple; mod topo; diff --git a/gix-traverse/tests/commit/ancestor.rs b/gix-traverse/tests/commit/simple.rs similarity index 95% rename from gix-traverse/tests/commit/ancestor.rs rename to gix-traverse/tests/commit/simple.rs index 6a9b2faa6c3..983941f35de 100644 --- a/gix-traverse/tests/commit/ancestor.rs +++ b/gix-traverse/tests/commit/simple.rs @@ -9,7 +9,7 @@ struct TraversalAssertion<'a> { tips: &'a [&'a str], expected: &'a [&'a str], mode: commit::Parents, - sorting: commit::Sorting, + sorting: commit::simple::Sorting, } impl<'a> TraversalAssertion<'a> { @@ -33,7 +33,7 @@ impl<'a> TraversalAssertion<'a> { self } - fn with_sorting(&mut self, sorting: commit::Sorting) -> &mut Self { + fn with_sorting(&mut self, sorting: commit::simple::Sorting) -> &mut Self { self.sorting = sorting; self } @@ -63,7 +63,7 @@ impl TraversalAssertion<'_> { let (store, tips, expected) = self.setup()?; for use_commitgraph in [false, true] { - let oids = commit::Ancestors::filtered(tips.clone(), &store, predicate.clone()) + 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)) @@ -79,7 +79,7 @@ impl TraversalAssertion<'_> { let (store, tips, expected) = self.setup()?; for use_commitgraph in [false, true] { - let oids = commit::Ancestors::new(tips.clone(), &store) + let oids = commit::Simple::new(tips.clone(), &store) .sorting(self.sorting)? .parents(self.mode) .commit_graph(self.setup_commitgraph(store.store_ref(), use_commitgraph)) @@ -92,9 +92,9 @@ impl TraversalAssertion<'_> { } mod different_date_intermixed { - use gix_traverse::commit::Sorting; + use gix_traverse::commit::simple::Sorting; - use crate::commit::ancestor::TraversalAssertion; + use crate::commit::simple::TraversalAssertion; #[test] fn head_breadth_first() -> crate::Result { @@ -140,9 +140,9 @@ mod different_date_intermixed { } mod different_date { - use gix_traverse::commit::Sorting; + use gix_traverse::commit::simple::Sorting; - use crate::commit::ancestor::TraversalAssertion; + use crate::commit::simple::TraversalAssertion; #[test] fn head_breadth_first() -> crate::Result { @@ -193,9 +193,9 @@ mod different_date { /// 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 gix_traverse::commit::{simple::Sorting, Parents}; - use crate::{commit::ancestor::TraversalAssertion, hex_to_id}; + use crate::{commit::simple::TraversalAssertion, hex_to_id}; #[test] fn c4_breadth_first() -> crate::Result { @@ -337,9 +337,9 @@ mod same_date { /// 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, Parents, Sorting}; + use gix_traverse::commit::{simple::Sorting, Parents, Simple}; - use crate::{commit::ancestor::TraversalAssertion, hex_to_id}; + use crate::{commit::simple::TraversalAssertion, hex_to_id}; #[test] fn head_breadth_first() -> crate::Result { @@ -390,7 +390,7 @@ mod adjusted_dates { 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( + let iter = Simple::new( Some(hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7" /* c2 */)), &store, ) diff --git a/gix/src/ext/object_id.rs b/gix/src/ext/object_id.rs index 018c0ab032b..44e03bbd7c9 100644 --- a/gix/src/ext/object_id.rs +++ b/gix/src/ext/object_id.rs @@ -1,9 +1,9 @@ use gix_hash::ObjectId; -use gix_traverse::commit::Ancestors; +use gix_traverse::commit::Simple; pub trait Sealed {} -pub type AncestorsIter = Ancestors bool>; +pub type AncestorsIter = Simple bool>; /// An extension trait to add functionality to [`ObjectId`]s. pub trait ObjectIdExt: Sealed { @@ -23,7 +23,7 @@ impl ObjectIdExt for ObjectId { where Find: gix_object::Find, { - Ancestors::new(Some(self), find) + Simple::new(Some(self), find) } fn attach(self, repo: &crate::Repository) -> crate::Id<'_> { 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 a089733a479..78e7c5c7497 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), + AncestorIter(#[from] gix_traverse::commit::simple::Error), #[error(transparent)] ShallowCommits(#[from] crate::shallow::open::Error), #[error(transparent)] @@ -166,7 +166,7 @@ impl<'repo> Platform<'repo> { Ok(revision::Walk { repo, inner: Box::new( - gix_traverse::commit::Ancestors::filtered(tips, &repo.objects, { + 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()?; @@ -221,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 From 1cfeb11f1fe9ad9c7b9084840ed7f5c5877f2f9a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 7 Apr 2024 16:16:52 +0200 Subject: [PATCH 10/10] adapt to changes in `gix-traverse` --- examples/log.rs | 2 +- gitoxide-core/src/repository/commitgraph/list.rs | 2 +- gitoxide-core/src/repository/revision/list.rs | 2 +- gix-diff/tests/tree/mod.rs | 3 +-- gix-pack/tests/pack/data/output/count_and_entries.rs | 3 +-- gix/src/ext/object_id.rs | 5 ++--- gix/src/remote/connection/fetch/update_refs/mod.rs | 2 +- gix/src/revision/spec/parse/delegate/navigate.rs | 12 +++--------- gix/src/revision/walk.rs | 10 +++++----- gix/tests/id/mod.rs | 10 +++++----- gix/tests/repository/shallow.rs | 2 +- 11 files changed, 22 insertions(+), 31 deletions(-) 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/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-diff/tests/tree/mod.rs b/gix-diff/tests/tree/mod.rs index bd57c5fc8dd..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::Simple::new(Some(head), &db) + gix_traverse::commit::Simple::new(Some(head), &db) .collect::, _>>() .expect("valid iteration") .into_iter() 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 f357f894798..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::Simple::new(Some(head), 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/src/ext/object_id.rs b/gix/src/ext/object_id.rs index 44e03bbd7c9..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::Simple; pub trait Sealed {} -pub type AncestorsIter = Simple bool>; +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, { - Simple::new(Some(self), 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/walk.rs b/gix/src/revision/walk.rs index 78e7c5c7497..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::simple::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 } 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/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};