diff --git a/gix-traverse/src/commit/simple.rs b/gix-traverse/src/commit/simple.rs index 7b851765bf0..3bdd4b3ea90 100644 --- a/gix-traverse/src/commit/simple.rs +++ b/gix-traverse/src/commit/simple.rs @@ -94,6 +94,8 @@ pub(super) struct State { /// /// As they may turn hidden later, we have to keep them until the conditions are met to return them. /// If `None`, there is nothing to do with hidden commits. + // TODO(perf): review this as we don't really need candidates anymore, given our current way of doing things. + // However, maybe they can see use when getting an incremental traversal done. candidates: Option, } @@ -212,45 +214,83 @@ mod init { /// Hide the given `tips`, along with all commits reachable by them so that they will not be returned /// by the traversal. /// - /// Note that this will force the traversal into a non-intermediate mode and queue return candidates, - /// to be released when it's clear that they truly are not hidden. + /// This function fully traverses all hidden tips and their ancestors, marking them as hidden + /// before iteration begins. This approach ensures correct behavior regardless + /// of graph topology or traversal order, matching git's `rev-list --not` behavior, + /// at great cost to performance, unfortunately. /// /// Note that hidden objects are expected to exist. + // TODO(perf): make this hiding iterative to avoid traversing the entire graph, always. pub fn hide(mut self, tips: impl IntoIterator) -> Result { - self.state.candidates = Some(VecDeque::new()); - let state = &mut self.state; - for id_to_ignore in tips { - let previous = state.seen.insert(id_to_ignore, CommitState::Hidden); - // If there was something, it will pick up whatever commit-state we have set last - // upon iteration. Also, hidden states always override everything else. - if previous.is_none() { - // Assure we *start* traversing hidden variants of a commit first, give them a head-start. - match self.sorting { - Sorting::BreadthFirst => { - state.next.push_front((id_to_ignore, CommitState::Hidden)); + // Collect hidden tips first + let hidden_tips: Vec = tips.into_iter().collect(); + if hidden_tips.is_empty() { + return Ok(self); + } + + // Fully traverse all hidden tips and mark all reachable commits as Hidden. + // This is "graph painting" - we paint all hidden commits upfront rather than + // interleaving hidden and interesting traversals, which ensures correct behavior + // regardless of graph topology or traversal order. + let mut queue: VecDeque = VecDeque::new(); + + for id_to_ignore in hidden_tips { + if self.state.seen.insert(id_to_ignore, CommitState::Hidden).is_none() { + queue.push_back(id_to_ignore); + } + } + + // Process all hidden commits and their ancestors + while let Some(id) = queue.pop_front() { + match super::super::find(self.cache.as_ref(), &self.objects, &id, &mut self.state.buf) { + Ok(Either::CachedCommit(commit)) => { + if !collect_parents(&mut self.state.parent_ids, self.cache.as_ref(), commit.iter_parents()) { + // drop corrupt caches and retry + self.cache = None; + // Re-add to queue to retry without cache + if self.state.seen.get(&id).is_some_and(CommitState::is_hidden) { + queue.push_back(id); + } + continue; } - Sorting::ByCommitTime(order) | Sorting::ByCommitTimeCutoff { order, .. } => { - add_to_queue( - id_to_ignore, - CommitState::Hidden, - order, - self.sorting.cutoff_time(), - &mut state.queue, - &self.objects, - &mut state.buf, - )?; + for (parent_id, _commit_time) in self.state.parent_ids.drain(..) { + if self.state.seen.insert(parent_id, CommitState::Hidden).is_none() { + queue.push_back(parent_id); + } } } + Ok(Either::CommitRefIter(commit_iter)) => { + for token in commit_iter { + match token { + Ok(gix_object::commit::ref_iter::Token::Tree { .. }) => continue, + Ok(gix_object::commit::ref_iter::Token::Parent { id: parent_id }) => { + if self.state.seen.insert(parent_id, CommitState::Hidden).is_none() { + queue.push_back(parent_id); + } + } + Ok(_unused_token) => break, + Err(err) => return Err(err.into()), + } + } + } + Err(err) => return Err(err.into()), } } - if !self - .state - .seen - .values() - .any(|state| matches!(state, CommitState::Hidden)) - { - self.state.candidates = None; - } + + // Now that all hidden commits are painted, we no longer need special handling + // during the main traversal. We can remove hidden commits from the main queues + // and simply skip them during iteration. + // + // Note: We don't need the candidates buffer anymore since hidden commits are + // pre-painted. But we keep it for compatibility with existing behavior and + // in case interesting commits were already queued before hide() was called. + self.state.candidates = None; + + // Remove any hidden commits from the interesting queues + self.state + .next + .retain(|(id, _)| !self.state.seen.get(id).is_some_and(CommitState::is_hidden)); + Ok(self) } diff --git a/gix-traverse/tests/fixtures/generated-archives/make_repo_for_hidden_bug.tar b/gix-traverse/tests/fixtures/generated-archives/make_repo_for_hidden_bug.tar new file mode 100644 index 00000000000..069be58cc5c Binary files /dev/null and b/gix-traverse/tests/fixtures/generated-archives/make_repo_for_hidden_bug.tar differ diff --git a/gix-traverse/tests/fixtures/make_repo_for_hidden_bug.sh b/gix-traverse/tests/fixtures/make_repo_for_hidden_bug.sh new file mode 100755 index 00000000000..748444a629e --- /dev/null +++ b/gix-traverse/tests/fixtures/make_repo_for_hidden_bug.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +function commit_at() { + local message=${1:?first argument is the commit message} + local timestamp=${2:?second argument is the timestamp} + GIT_COMMITTER_DATE="$timestamp -0700" + GIT_AUTHOR_DATE="$timestamp -0700" + export GIT_COMMITTER_DATE GIT_AUTHOR_DATE + git commit --allow-empty -m "$message" +} + +function optimize() { + git commit-graph write --no-progress --reachable + git repack -adq +} + +# Test 1: Hidden traversal has a longer path to shared ancestors +# Graph structure: +# A(tip) --> shared +# / +# H(hidden) --> X --> Y --> shared +# +# This tests that shared is correctly hidden even though the interesting +# path (A->shared) is shorter than the hidden path (H->X->Y->shared). + +(git init long_hidden_path && cd long_hidden_path + git checkout -b main + + # Create base commit with oldest timestamp + commit_at "shared" 1000000000 + + # Create hidden branch with intermediate commits + git checkout -b hidden_branch + commit_at "Y" 1000000100 + commit_at "X" 1000000200 + commit_at "H" 1000000300 # hidden tip + + # Go back to main and create tip A (newest timestamp) + git checkout main + commit_at "A" 1000000400 # tip + + optimize +) + +# Test 2: Similar structure but with interesting path longer than hidden path +# Graph structure: +# A(tip) --> B --> C --> D(shared) +# / +# H(hidden) --------->+ +# +# This tests that D is correctly hidden when the interesting path +# (A->B->C->D) is longer than the hidden path (H->D). + +(git init long_interesting_path && cd long_interesting_path + git checkout -b main + + # Create base commit with oldest timestamp + commit_at "D" 1000000000 + + # Create hidden branch (direct to D) + git checkout -b hidden_branch + commit_at "H" 1000000100 # hidden tip, direct child of D + + # Go back to main and create longer path + git checkout main + commit_at "C" 1000000200 + commit_at "B" 1000000300 + commit_at "A" 1000000400 # tip + + optimize +) diff --git a/gix-traverse/tests/traverse/commit/simple.rs b/gix-traverse/tests/traverse/commit/simple.rs deleted file mode 100644 index 3529f101985..00000000000 --- a/gix-traverse/tests/traverse/commit/simple.rs +++ /dev/null @@ -1,756 +0,0 @@ -use crate::hex_to_id; -use gix_hash::{oid, ObjectId}; -use gix_object::bstr::{ByteSlice, ByteVec}; -use gix_traverse::commit; -use std::path::PathBuf; - -struct TraversalAssertion<'a> { - init_script: &'a str, - repo_name: &'a str, - tips: &'a [&'a str], - expected: &'a [&'a str], - mode: commit::Parents, - sorting: commit::simple::Sorting, - expected_without_tips: bool, - // commit-ids that should be hidden (along with all their history. - hidden: &'a [&'a str], -} - -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(), - hidden: Default::default(), - expected_without_tips: false, - } - } - - fn with_parents(&mut self, mode: commit::Parents) -> &mut Self { - self.mode = mode; - self - } - - fn with_sorting(&mut self, sorting: commit::simple::Sorting) -> &mut Self { - self.sorting = sorting; - self - } - - /// Set the commits that should be hidden. - fn with_hidden(&mut self, hidden: &'a [&'a str]) -> &mut Self { - self.hidden = hidden; - self - } - - /// Do not automatically add tips to the set of expected items. - fn expected_without_tips(&mut self) -> &mut Self { - self.expected_without_tips = true; - self - } - - /// Execute the fixture and get the repository worktree path. - pub fn worktree_dir(&self) -> crate::Result { - let dir = gix_testtools::scripted_fixture_read_only_standalone(self.init_script)?; - Ok(dir.join(self.repo_name)) - } -} - -impl TraversalAssertion<'_> { - #[allow(clippy::type_complexity)] - fn setup(&self) -> crate::Result<(gix_odb::Handle, Vec, Vec, Vec)> { - let repo_path = self.worktree_dir()?; - let store = gix_odb::at(repo_path.join(".git").join("objects"))?; - let tips: Vec<_> = self.tips.iter().copied().map(hex_to_id).collect(); - let expected: Vec = if self.expected_without_tips { - self.expected.iter().map(|hex_id| hex_to_id(hex_id)).collect() - } else { - tips.clone() - .into_iter() - .chain(self.expected.iter().map(|hex_id| hex_to_id(hex_id))) - .collect() - }; - let hidden: Vec<_> = self.hidden.iter().copied().map(hex_to_id).collect(); - Ok((store, tips, expected, hidden)) - } - - 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, hidden) = self.setup()?; - - for use_commitgraph in [false, true] { - let oids = commit::Simple::filtered(tips.clone(), &store, predicate.clone()) - .sorting(self.sorting)? - .parents(self.mode) - .hide(hidden.clone())? - .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, hidden) = self.setup()?; - - for use_commitgraph in [false, true] { - let oids = commit::Simple::new(tips.clone(), &store) - .sorting(self.sorting)? - .parents(self.mode) - .hide(hidden.clone())? - .commit_graph(self.setup_commitgraph(store.store_ref(), use_commitgraph)) - .map(|res| res.map(|info| info.id)) - .collect::, _>>()?; - assert_eq!( - oids, expected, - "use_commitgraph = {use_commitgraph}, sorting = {:?}", - self.sorting - ); - } - Ok(()) - } -} - -mod hide { - use crate::commit::simple::{git_graph, TraversalAssertion}; - use gix_traverse::commit::simple::{CommitTimeOrder, Sorting}; - use gix_traverse::commit::Parents; - - fn all_sortings() -> impl Iterator { - [ - Sorting::BreadthFirst, - Sorting::ByCommitTime(CommitTimeOrder::NewestFirst), - Sorting::ByCommitTime(CommitTimeOrder::OldestFirst), - ] - .into_iter() - } - - #[test] - fn disjoint_hidden_and_interesting() -> crate::Result { - let mut assertion = TraversalAssertion::new_at( - "make_repos.sh", - "disjoint_branches", - &["e07cf1277ff7c43090f1acfc85a46039e7de1272"], /* b3 */ - &[ - "94cf3f3a4c782b672173423e7a4157a02957dd48", /* b2 */ - "34e5ff5ce3d3ba9f0a00d11a7fad72551fff0861", /* b1 */ - ], - ); - insta::assert_snapshot!(git_graph(assertion.worktree_dir()?)?, @r" - * e07cf1277ff7c43090f1acfc85a46039e7de1272 (HEAD -> disjoint) b3 - * 94cf3f3a4c782b672173423e7a4157a02957dd48 b2 - * 34e5ff5ce3d3ba9f0a00d11a7fad72551fff0861 b1 - * b5665181bf4c338ab16b10da0524d81b96aff209 (main) a3 - * f0230ce37b83d8e9f51ea6322ed7e8bd148d8e28 a2 - * 674aca0765b935ac5e7f7e9ab83af7f79272b5b0 a1 - "); - - for sorting in all_sortings() { - assertion - .with_hidden(&["b5665181bf4c338ab16b10da0524d81b96aff209" /* a3 */]) - .with_sorting(sorting) - .check()?; - } - Ok(()) - } - - #[test] - fn all_hidden() -> crate::Result { - let mut assertion = TraversalAssertion::new_at( - "make_repos.sh", - "disjoint_branches", - &[ - "e07cf1277ff7c43090f1acfc85a46039e7de1272", /* b3 */ - "b5665181bf4c338ab16b10da0524d81b96aff209", /* a3 */ - ], - // The start positions are also declared hidden, so nothing should be visible. - &[], - ); - insta::assert_snapshot!(git_graph(assertion.worktree_dir()?)?, @r" - * e07cf1277ff7c43090f1acfc85a46039e7de1272 (HEAD -> disjoint) b3 - * 94cf3f3a4c782b672173423e7a4157a02957dd48 b2 - * 34e5ff5ce3d3ba9f0a00d11a7fad72551fff0861 b1 - * b5665181bf4c338ab16b10da0524d81b96aff209 (main) a3 - * f0230ce37b83d8e9f51ea6322ed7e8bd148d8e28 a2 - * 674aca0765b935ac5e7f7e9ab83af7f79272b5b0 a1 - "); - - for sorting in all_sortings() { - assertion - .with_hidden(&[ - "e07cf1277ff7c43090f1acfc85a46039e7de1272", /* b3 */ - "b5665181bf4c338ab16b10da0524d81b96aff209", /* a3 */ - ]) - .with_sorting(sorting) - .expected_without_tips() - .check()?; - } - Ok(()) - } - - #[test] - fn some_hidden_and_all_hidden() -> crate::Result { - // Hidden has to catch up with non-hidden. - let mut assertion = TraversalAssertion::new_at( - "make_repos.sh", - "simple", - &["ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83"], /* c2 */ - &[], - ); - - insta::assert_snapshot!(git_graph(assertion.worktree_dir()?)?, @r" - *-. f49838d84281c3988eeadd988d97dd358c9f9dc4 (HEAD -> main) merge - |\ \ - | | * 48e8dac19508f4238f06c8de2b10301ce64a641c (branch2) b2c2 - | | * cb6a6befc0a852ac74d74e0354e0f004af29cb79 b2c1 - | * | 66a309480201c4157b0eae86da69f2d606aadbe7 (branch1) b1c2 - | * | 80947acb398362d8236fcb8bf0f8a9dac640583f b1c1 - | |/ - * / 0edb95c0c0d9933d88f532ec08fcd405d0eee882 c5 - |/ - * 8cb5f13b66ce52a49399a2c49f537ee2b812369c c4 - * 33aa07785dd667c0196064e3be3c51dd9b4744ef c3 - * ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83 c2 - * 65d6af66f60b8e39fd1ba6a1423178831e764ec5 c1 - "); - - for sorting in all_sortings() { - assertion - .with_hidden(&["0edb95c0c0d9933d88f532ec08fcd405d0eee882" /* c5 */]) - .expected_without_tips() - .with_sorting(sorting) - .check()?; - } - let mut assertion = TraversalAssertion::new_at( - "make_repos.sh", - "simple", - &["f49838d84281c3988eeadd988d97dd358c9f9dc4"], /* merge */ - &["0edb95c0c0d9933d88f532ec08fcd405d0eee882" /* c5 */], - ); - - for sorting in all_sortings() { - assertion - .with_hidden(&[ - "48e8dac19508f4238f06c8de2b10301ce64a641c", /* b2c2 */ - "66a309480201c4157b0eae86da69f2d606aadbe7", /* b1c2 */ - ]) - .with_sorting(sorting) - .check()?; - } - - let mut assertion = TraversalAssertion::new_at( - "make_repos.sh", - "simple", - &["80947acb398362d8236fcb8bf0f8a9dac640583f"], /* b1c1 */ - // Single-parent is only for commits that we are/ought to be interested in. - // Hence, hidden commits still catch up. - &[], - ); - - assertion - .with_hidden(&["f49838d84281c3988eeadd988d97dd358c9f9dc4" /* merge */]) - .with_parents(Parents::First) - .expected_without_tips() - .check()?; - Ok(()) - } -} - -mod different_date_intermixed { - use gix_traverse::commit::simple::{CommitTimeOrder, Sorting}; - - use crate::commit::simple::TraversalAssertion; - - #[test] - fn head_breadth_first() -> crate::Result { - TraversalAssertion::new_at( - "make_repos.sh", - "intermixed", - &["58912d92944087dcb09dca79cdd2a937cc158bed"], /* merge */ - // This is very different from what git does as it keeps commits together, - // whereas we spread them out breadth-first. - &[ - "2dce37be587e07caef8c4a5ab60b423b13a8536a", /* c3 */ - "0f6632a5a7d81417488b86692b729e49c1b73056", /* b1c2 */ - "a9c28710e058af4e5163699960234adb9fb2abc7", /* b2c2 */ - "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ - "77fd3c6832c0cd542f7a39f3af9250c3268db979", /* b1c1 */ - "b648f955b930ca95352fae6f22cb593ee0244b27", /* b2c1 */ - "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ - ], - ) - .with_sorting(Sorting::BreadthFirst) - .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::ByCommitTime(CommitTimeOrder::NewestFirst)) - .check()?; - - TraversalAssertion::new_at( - "make_repos.sh", - "intermixed", - &["58912d92944087dcb09dca79cdd2a937cc158bed"], /* merge */ - &[ - "a9c28710e058af4e5163699960234adb9fb2abc7", /* b2c2 */ - "b648f955b930ca95352fae6f22cb593ee0244b27", /* b2c1 */ - "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ - "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ - "0f6632a5a7d81417488b86692b729e49c1b73056", /* b1c2 */ - "77fd3c6832c0cd542f7a39f3af9250c3268db979", /* b1c1 */ - "2dce37be587e07caef8c4a5ab60b423b13a8536a", /* c3 */ - ], - ) - .with_sorting(Sorting::ByCommitTime(CommitTimeOrder::OldestFirst)) - .check() - } -} - -mod different_date { - use gix_traverse::commit::simple::{CommitTimeOrder, Sorting}; - - use crate::commit::simple::TraversalAssertion; - - #[test] - fn head_breadth_first() -> crate::Result { - TraversalAssertion::new_at( - "make_repos.sh", - "simple", - &["f49838d84281c3988eeadd988d97dd358c9f9dc4"], /* merge */ - // This is very different from what git does as it keeps commits together, - // whereas we spread them out breadth-first. - &[ - "0edb95c0c0d9933d88f532ec08fcd405d0eee882", /* c5 */ - "66a309480201c4157b0eae86da69f2d606aadbe7", /* b1c2 */ - "48e8dac19508f4238f06c8de2b10301ce64a641c", /* b2c2 */ - "8cb5f13b66ce52a49399a2c49f537ee2b812369c", /* c4 */ - "80947acb398362d8236fcb8bf0f8a9dac640583f", /* b1c1 */ - "cb6a6befc0a852ac74d74e0354e0f004af29cb79", /* b2c1 */ - "33aa07785dd667c0196064e3be3c51dd9b4744ef", /* c3 */ - "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ - "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ - ], - ) - .check() - } - - #[test] - fn head_date_order() -> crate::Result { - TraversalAssertion::new_at( - "make_repos.sh", - "simple", - &["f49838d84281c3988eeadd988d97dd358c9f9dc4"], /* merge */ - // This is exactly what git shows. - &[ - "0edb95c0c0d9933d88f532ec08fcd405d0eee882", /* c5 */ - "66a309480201c4157b0eae86da69f2d606aadbe7", /* b1c2 */ - "80947acb398362d8236fcb8bf0f8a9dac640583f", /* b1c1 */ - "48e8dac19508f4238f06c8de2b10301ce64a641c", /* b2c2 */ - "cb6a6befc0a852ac74d74e0354e0f004af29cb79", /* b2c1 */ - "8cb5f13b66ce52a49399a2c49f537ee2b812369c", /* c4 */ - "33aa07785dd667c0196064e3be3c51dd9b4744ef", /* c3 */ - "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ - "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ - ], - ) - .with_sorting(Sorting::ByCommitTime(CommitTimeOrder::NewestFirst)) - .check()?; - TraversalAssertion::new_at( - "make_repos.sh", - "simple", - &["f49838d84281c3988eeadd988d97dd358c9f9dc4"], /* merge */ - &[ - "48e8dac19508f4238f06c8de2b10301ce64a641c", /* b2c2 */ - "cb6a6befc0a852ac74d74e0354e0f004af29cb79", /* b2c1 */ - "8cb5f13b66ce52a49399a2c49f537ee2b812369c", /* c4 */ - "33aa07785dd667c0196064e3be3c51dd9b4744ef", /* c3 */ - "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ - "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ - "66a309480201c4157b0eae86da69f2d606aadbe7", /* b1c2 */ - "80947acb398362d8236fcb8bf0f8a9dac640583f", /* b1c1 */ - "0edb95c0c0d9933d88f532ec08fcd405d0eee882", /* c5 */ - ], - ) - .with_sorting(Sorting::ByCommitTime(CommitTimeOrder::OldestFirst)) - .check() - } -} - -/// Same dates are somewhat special as they show how sorting-details on priority queues affects ordering -mod same_date { - use gix_traverse::commit::{ - simple::{CommitTimeOrder, Sorting}, - Parents, - }; - - use crate::{commit::simple::TraversalAssertion, hex_to_id}; - - #[test] - fn c4_breadth_first() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_same_date.sh", - &["9556057aee5abb06912922e9f26c46386a816822"], /* c4 */ - &[ - "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .with_sorting(Sorting::BreadthFirst) - .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 */ - ], - ) - .with_sorting(Sorting::BreadthFirst) - .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::ByCommitTime(CommitTimeOrder::NewestFirst)) - .check()?; - - 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::ByCommitTime(CommitTimeOrder::OldestFirst)) - .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) - .with_sorting(Sorting::BreadthFirst) - .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 */ - ], - ) - .with_sorting(Sorting::BreadthFirst) - .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 */ - ], - ) - .with_sorting(Sorting::BreadthFirst) - .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 */ - ], - ) - .with_sorting(Sorting::BreadthFirst) - .check_with_predicate(move |id| { - if id == hex_to_id("9556057aee5abb06912922e9f26c46386a816822") { - assert!(!seen); - seen = true; - false - } else { - true - } - }) - } -} - -/// Some dates adjusted to be a year apart, but still 'c1' and 'c2' with the same date. -mod adjusted_dates { - use gix_traverse::commit::{ - simple::{CommitTimeOrder, Sorting}, - Parents, Simple, - }; - - use crate::commit::simple::git_graph; - use crate::{commit::simple::TraversalAssertion, hex_to_id}; - - #[test] - fn head_breadth_first() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_with_dates.sh", - &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ - // Here `git` also shows `b1c1` first, making topo-order similar to date order for some reason, - // even though c2 *is* the first parent. - &[ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac", /* b1c1 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .with_sorting(Sorting::BreadthFirst) - .check() - } - - #[test] - fn head_date_order() -> crate::Result { - let mut assertion = TraversalAssertion::new( - "make_traversal_repo_for_commits_with_dates.sh", - &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ - &[ - "bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac", /* b1c1 */ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ); - insta::assert_snapshot!(git_graph(assertion.worktree_dir()?)?, @r" - * 288e509293165cb5630d08f4185bdf2445bf6170 (HEAD -> main) m1b1 - |\ - | * bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac (branch1) b1c1 - * | 9902e3c3e8f0c569b4ab295ddf473e6de763e1e7 c2 - |/ - * 134385f6d781b7e97062102c6a483440bfda2a03 c1 - "); - assertion - .with_sorting(Sorting::ByCommitTime(CommitTimeOrder::NewestFirst)) - .check()?; - TraversalAssertion::new( - "make_traversal_repo_for_commits_with_dates.sh", - &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ - &[ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - "bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac", /* b1c1 */ - ], - ) - .with_sorting(Sorting::ByCommitTime(CommitTimeOrder::OldestFirst)) - .check() - } - - #[test] - fn head_date_order_with_cutoff() -> crate::Result { - for order in all_commit_time_orderings() { - TraversalAssertion::new( - "make_traversal_repo_for_commits_with_dates.sh", - &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ - &["bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac"], /* b1c1 */ - ) - .with_sorting(Sorting::ByCommitTimeCutoff { - order, - seconds: 978393600, // =2001-01-02 00:00:00 +0000 - }) - .check()?; - } - Ok(()) - } - - #[test] - fn head_date_order_with_cutoff_disabled() -> crate::Result { - let very_early = 878393600; // an early date before any commit - TraversalAssertion::new( - "make_traversal_repo_for_commits_with_dates.sh", - &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ - &[ - "bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac", /* b1c1 */ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .with_sorting(Sorting::ByCommitTimeCutoff { - order: CommitTimeOrder::NewestFirst, - seconds: very_early, - }) - .check()?; - - TraversalAssertion::new( - "make_traversal_repo_for_commits_with_dates.sh", - &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ - &[ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - "bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac", /* b1c1 */ - ], - ) - .with_sorting(Sorting::ByCommitTimeCutoff { - order: CommitTimeOrder::OldestFirst, - seconds: very_early, - }) - .check()?; - Ok(()) - } - - #[test] - fn date_order_with_cutoff_is_applied_to_starting_position() -> crate::Result { - for order in all_commit_time_orderings() { - let dir = - gix_testtools::scripted_fixture_read_only_standalone("make_traversal_repo_for_commits_with_dates.sh")?; - let store = gix_odb::at(dir.join(".git").join("objects"))?; - let iter = Simple::new( - Some(hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7" /* c2 */)), - &store, - ) - .sorting(Sorting::ByCommitTimeCutoff { - order, - 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 { - for order in all_commit_time_orderings() { - TraversalAssertion::new( - "make_traversal_repo_for_commits_with_dates.sh", - &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ - &[ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .with_sorting(Sorting::ByCommitTime(order)) - .with_parents(Parents::First) - .check()?; - } - Ok(()) - } - - fn all_commit_time_orderings() -> [CommitTimeOrder; 2] { - [CommitTimeOrder::NewestFirst, CommitTimeOrder::OldestFirst] - } -} - -/// Execute a git status in the given repository path. -fn git_graph(repo_dir: impl AsRef) -> crate::Result { - let out = std::process::Command::new(gix_path::env::exe_invocation()) - .current_dir(repo_dir) - .args([ - "log", - "--oneline", - "--graph", - "--decorate", - "--all", - "--pretty=format:%H %d %s", - ]) - .output()?; - if !out.status.success() { - return Err(format!("git status failed: {err}", err = out.stderr.to_str_lossy()).into()); - } - Ok(out.stdout.into_string_lossy()) -} diff --git a/gix-traverse/tests/traverse/commit/simple/adjusted_dates.rs b/gix-traverse/tests/traverse/commit/simple/adjusted_dates.rs new file mode 100644 index 00000000000..b2fecf8dda7 --- /dev/null +++ b/gix-traverse/tests/traverse/commit/simple/adjusted_dates.rs @@ -0,0 +1,199 @@ +//! Some dates adjusted to be a year apart, but still 'c1' and 'c2' with the same date. +use super::*; +use crate::util::fixture; +use gix_traverse::commit::simple::CommitTimeOrder; + +fn adjusted_dates_repo() -> crate::Result<(std::path::PathBuf, gix_odb::Handle)> { + let dir = fixture("make_traversal_repo_for_commits_with_dates.sh")?; + let odb = gix_odb::at(dir.join(".git").join("objects"))?; + Ok((dir, odb)) +} + +#[test] +fn head_breadth_first() -> crate::Result { + let (repo_dir, odb) = adjusted_dates_repo()?; + + // Timestamps show b1c1 (978393600) is a year newer than c2 (946771200), + // explaining why date-order puts b1c1 before c2. + insta::assert_snapshot!(git_graph_with_time(&repo_dir)?, @r" + * 288e509293165cb5630d08f4185bdf2445bf6170 1009929600 (HEAD -> main) m1b1 + |\ + | * bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac 978393600 (branch1) b1c1 + * | 9902e3c3e8f0c569b4ab295ddf473e6de763e1e7 946771200 c2 + |/ + * 134385f6d781b7e97062102c6a483440bfda2a03 946771200 c1 + "); + + let tip = hex_to_id("288e509293165cb5630d08f4185bdf2445bf6170"); // m1b1 + + // Git also shows `b1c1` first, making topo-order similar to date order, + // even though c2 *is* the first parent. + let expected = [ + tip, + hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7"), // c2 + hex_to_id("bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac"), // b1c1 + hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"), // c1 + ]; + + let result = traverse_both([tip], &odb, Sorting::BreadthFirst, Parents::All, [])?; + assert_eq!(result, expected); + Ok(()) +} + +#[test] +fn head_date_order() -> crate::Result { + let (_repo_dir, odb) = adjusted_dates_repo()?; + // Graph with timestamps shown in `head_breadth_first` + let tip = hex_to_id("288e509293165cb5630d08f4185bdf2445bf6170"); // m1b1 + + // NewestFirst + let expected_newest = [ + tip, + hex_to_id("bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac"), // b1c1 + hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7"), // c2 + hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"), // c1 + ]; + let result = traverse_both( + [tip], + &odb, + Sorting::ByCommitTime(CommitTimeOrder::NewestFirst), + Parents::All, + [], + )?; + assert_eq!(result, expected_newest); + + // OldestFirst + let expected_oldest = [ + tip, + hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7"), // c2 + hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"), // c1 + hex_to_id("bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac"), // b1c1 + ]; + let result = traverse_both( + [tip], + &odb, + Sorting::ByCommitTime(CommitTimeOrder::OldestFirst), + Parents::All, + [], + )?; + assert_eq!(result, expected_oldest); + + Ok(()) +} + +#[test] +fn head_date_order_with_cutoff() -> crate::Result { + let (_repo_dir, odb) = adjusted_dates_repo()?; + // Graph shown in `head_breadth_first` + let tip = hex_to_id("288e509293165cb5630d08f4185bdf2445bf6170"); // m1b1 + + let expected = [ + tip, + hex_to_id("bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac"), // b1c1 + ]; + + for order in [CommitTimeOrder::NewestFirst, CommitTimeOrder::OldestFirst] { + let result = traverse_both( + [tip], + &odb, + Sorting::ByCommitTimeCutoff { + order, + seconds: 978393600, // =2001-01-02 00:00:00 +0000 + }, + Parents::All, + [], + )?; + assert_eq!(result, expected, "order = {order:?}"); + } + Ok(()) +} + +#[test] +fn head_date_order_with_cutoff_disabled() -> crate::Result { + let (_repo_dir, odb) = adjusted_dates_repo()?; + // Graph shown in `head_breadth_first` + let tip = hex_to_id("288e509293165cb5630d08f4185bdf2445bf6170"); // m1b1 + let very_early = 878393600; // an early date before any commit + + // NewestFirst with early cutoff (effectively disabled) + let expected_newest = [ + tip, + hex_to_id("bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac"), // b1c1 + hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7"), // c2 + hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"), // c1 + ]; + let result = traverse_both( + [tip], + &odb, + Sorting::ByCommitTimeCutoff { + order: CommitTimeOrder::NewestFirst, + seconds: very_early, + }, + Parents::All, + [], + )?; + assert_eq!(result, expected_newest); + + // OldestFirst with early cutoff + let expected_oldest = [ + tip, + hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7"), // c2 + hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"), // c1 + hex_to_id("bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac"), // b1c1 + ]; + let result = traverse_both( + [tip], + &odb, + Sorting::ByCommitTimeCutoff { + order: CommitTimeOrder::OldestFirst, + seconds: very_early, + }, + Parents::All, + [], + )?; + assert_eq!(result, expected_oldest); + + Ok(()) +} + +#[test] +fn date_order_with_cutoff_is_applied_to_starting_position() -> crate::Result { + let (_repo_dir, odb) = adjusted_dates_repo()?; + // Graph shown in `head_breadth_first` + let tip = hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7"); // c2 + + for order in [CommitTimeOrder::NewestFirst, CommitTimeOrder::OldestFirst] { + let graph = commit_graph(odb.store_ref()); + let count = Simple::new([tip], &odb) + .sorting(Sorting::ByCommitTimeCutoff { + order, + seconds: 978393600, // =2001-01-02 00:00:00 +0000 + })? + .commit_graph(graph) + .count(); + assert_eq!( + 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 { + let (_repo_dir, odb) = adjusted_dates_repo()?; + // Graph shown in `head_breadth_first` + let tip = hex_to_id("288e509293165cb5630d08f4185bdf2445bf6170"); // m1b1 + + let expected = [ + tip, + hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7"), // c2 + hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"), // c1 + ]; + + for order in [CommitTimeOrder::NewestFirst, CommitTimeOrder::OldestFirst] { + let result = traverse_both([tip], &odb, Sorting::ByCommitTime(order), Parents::First, [])?; + assert_eq!(result, expected, "order = {order:?}"); + } + Ok(()) +} diff --git a/gix-traverse/tests/traverse/commit/simple/different_date.rs b/gix-traverse/tests/traverse/commit/simple/different_date.rs new file mode 100644 index 00000000000..dafc18d6664 --- /dev/null +++ b/gix-traverse/tests/traverse/commit/simple/different_date.rs @@ -0,0 +1,102 @@ +use super::*; +use gix_traverse::commit::simple::CommitTimeOrder; + +fn simple_repo() -> crate::Result<(std::path::PathBuf, gix_odb::Handle)> { + named_fixture("make_repos.sh", "simple") +} + +#[test] +fn head_breadth_first() -> crate::Result { + let (repo_dir, odb) = simple_repo()?; + + // Timestamps show branch1 commits are newer than branch2, with c5 being the newest. + insta::assert_snapshot!(git_graph_with_time(&repo_dir)?, @r" + *-. f49838d84281c3988eeadd988d97dd358c9f9dc4 1112912533 (HEAD -> main) merge + |\ \ + | | * 48e8dac19508f4238f06c8de2b10301ce64a641c 1112912353 (branch2) b2c2 + | | * cb6a6befc0a852ac74d74e0354e0f004af29cb79 1112912293 b2c1 + | * | 66a309480201c4157b0eae86da69f2d606aadbe7 1112912473 (branch1) b1c2 + | * | 80947acb398362d8236fcb8bf0f8a9dac640583f 1112912413 b1c1 + | |/ + * / 0edb95c0c0d9933d88f532ec08fcd405d0eee882 1112912533 c5 + |/ + * 8cb5f13b66ce52a49399a2c49f537ee2b812369c 1112912233 c4 + * 33aa07785dd667c0196064e3be3c51dd9b4744ef 1112912173 c3 + * ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83 1112912113 c2 + * 65d6af66f60b8e39fd1ba6a1423178831e764ec5 1112912053 c1 + "); + + let tip = hex_to_id("f49838d84281c3988eeadd988d97dd358c9f9dc4"); // merge + + // This is very different from what git does as it keeps commits together, + // whereas we spread them out breadth-first. + let expected = [ + tip, + hex_to_id("0edb95c0c0d9933d88f532ec08fcd405d0eee882"), // c5 + hex_to_id("66a309480201c4157b0eae86da69f2d606aadbe7"), // b1c2 + hex_to_id("48e8dac19508f4238f06c8de2b10301ce64a641c"), // b2c2 + hex_to_id("8cb5f13b66ce52a49399a2c49f537ee2b812369c"), // c4 + hex_to_id("80947acb398362d8236fcb8bf0f8a9dac640583f"), // b1c1 + hex_to_id("cb6a6befc0a852ac74d74e0354e0f004af29cb79"), // b2c1 + hex_to_id("33aa07785dd667c0196064e3be3c51dd9b4744ef"), // c3 + hex_to_id("ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83"), // c2 + hex_to_id("65d6af66f60b8e39fd1ba6a1423178831e764ec5"), // c1 + ]; + + let result = traverse_both([tip], &odb, Sorting::BreadthFirst, Parents::All, [])?; + assert_eq!(result, expected); + Ok(()) +} + +#[test] +fn head_date_order() -> crate::Result { + let (_repo_dir, odb) = simple_repo()?; + // Graph with timestamps shown in `head_breadth_first` + let tip = hex_to_id("f49838d84281c3988eeadd988d97dd358c9f9dc4"); // merge + + // NewestFirst - exactly what git shows + let expected_newest = [ + tip, + hex_to_id("0edb95c0c0d9933d88f532ec08fcd405d0eee882"), // c5 + hex_to_id("66a309480201c4157b0eae86da69f2d606aadbe7"), // b1c2 + hex_to_id("80947acb398362d8236fcb8bf0f8a9dac640583f"), // b1c1 + hex_to_id("48e8dac19508f4238f06c8de2b10301ce64a641c"), // b2c2 + hex_to_id("cb6a6befc0a852ac74d74e0354e0f004af29cb79"), // b2c1 + hex_to_id("8cb5f13b66ce52a49399a2c49f537ee2b812369c"), // c4 + hex_to_id("33aa07785dd667c0196064e3be3c51dd9b4744ef"), // c3 + hex_to_id("ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83"), // c2 + hex_to_id("65d6af66f60b8e39fd1ba6a1423178831e764ec5"), // c1 + ]; + let result = traverse_both( + [tip], + &odb, + Sorting::ByCommitTime(CommitTimeOrder::NewestFirst), + Parents::All, + [], + )?; + assert_eq!(result, expected_newest); + + // OldestFirst + let expected_oldest = [ + tip, + hex_to_id("48e8dac19508f4238f06c8de2b10301ce64a641c"), // b2c2 + hex_to_id("cb6a6befc0a852ac74d74e0354e0f004af29cb79"), // b2c1 + hex_to_id("8cb5f13b66ce52a49399a2c49f537ee2b812369c"), // c4 + hex_to_id("33aa07785dd667c0196064e3be3c51dd9b4744ef"), // c3 + hex_to_id("ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83"), // c2 + hex_to_id("65d6af66f60b8e39fd1ba6a1423178831e764ec5"), // c1 + hex_to_id("66a309480201c4157b0eae86da69f2d606aadbe7"), // b1c2 + hex_to_id("80947acb398362d8236fcb8bf0f8a9dac640583f"), // b1c1 + hex_to_id("0edb95c0c0d9933d88f532ec08fcd405d0eee882"), // c5 + ]; + let result = traverse_both( + [tip], + &odb, + Sorting::ByCommitTime(CommitTimeOrder::OldestFirst), + Parents::All, + [], + )?; + assert_eq!(result, expected_oldest); + + Ok(()) +} diff --git a/gix-traverse/tests/traverse/commit/simple/different_date_intermixed.rs b/gix-traverse/tests/traverse/commit/simple/different_date_intermixed.rs new file mode 100644 index 00000000000..e81efe6e2c4 --- /dev/null +++ b/gix-traverse/tests/traverse/commit/simple/different_date_intermixed.rs @@ -0,0 +1,95 @@ +use super::*; +use gix_traverse::commit::simple::CommitTimeOrder; + +fn intermixed_repo() -> crate::Result<(std::path::PathBuf, gix_odb::Handle)> { + named_fixture("make_repos.sh", "intermixed") +} + +#[test] +fn head_breadth_first() -> crate::Result { + let (repo_dir, odb) = intermixed_repo()?; + + // Timestamps show the intermixed ordering: b1 and b2 commits are interleaved + // with main branch commits by time. + insta::assert_snapshot!(git_graph_with_time(&repo_dir)?, @r" + *-. 58912d92944087dcb09dca79cdd2a937cc158bed 1112912413 (HEAD -> main) merge + |\ \ + | | * a9c28710e058af4e5163699960234adb9fb2abc7 1112912293 (branch2) b2c2 + | | * b648f955b930ca95352fae6f22cb593ee0244b27 1112912173 b2c1 + | * | 0f6632a5a7d81417488b86692b729e49c1b73056 1112912353 (branch1) b1c2 + | * | 77fd3c6832c0cd542f7a39f3af9250c3268db979 1112912233 b1c1 + | |/ + * / 2dce37be587e07caef8c4a5ab60b423b13a8536a 1112912413 c3 + |/ + * ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83 1112912113 c2 + * 65d6af66f60b8e39fd1ba6a1423178831e764ec5 1112912053 c1 + "); + + let tip = hex_to_id("58912d92944087dcb09dca79cdd2a937cc158bed"); // merge + + // This is very different from what git does as it keeps commits together, + // whereas we spread them out breadth-first. + let expected = [ + tip, + hex_to_id("2dce37be587e07caef8c4a5ab60b423b13a8536a"), // c3 + hex_to_id("0f6632a5a7d81417488b86692b729e49c1b73056"), // b1c2 + hex_to_id("a9c28710e058af4e5163699960234adb9fb2abc7"), // b2c2 + hex_to_id("ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83"), // c2 + hex_to_id("77fd3c6832c0cd542f7a39f3af9250c3268db979"), // b1c1 + hex_to_id("b648f955b930ca95352fae6f22cb593ee0244b27"), // b2c1 + hex_to_id("65d6af66f60b8e39fd1ba6a1423178831e764ec5"), // c1 + ]; + + let result = traverse_both([tip], &odb, Sorting::BreadthFirst, Parents::All, [])?; + assert_eq!(result, expected); + Ok(()) +} + +#[test] +fn head_date_order() -> crate::Result { + let (_repo_dir, odb) = intermixed_repo()?; + // Graph with timestamps shown in `head_breadth_first` + let tip = hex_to_id("58912d92944087dcb09dca79cdd2a937cc158bed"); // merge + + // NewestFirst - exactly what git shows + let expected_newest = [ + tip, + hex_to_id("2dce37be587e07caef8c4a5ab60b423b13a8536a"), // c3 + hex_to_id("0f6632a5a7d81417488b86692b729e49c1b73056"), // b1c2 + hex_to_id("a9c28710e058af4e5163699960234adb9fb2abc7"), // b2c2 + hex_to_id("77fd3c6832c0cd542f7a39f3af9250c3268db979"), // b1c1 + hex_to_id("b648f955b930ca95352fae6f22cb593ee0244b27"), // b2c1 + hex_to_id("ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83"), // c2 + hex_to_id("65d6af66f60b8e39fd1ba6a1423178831e764ec5"), // c1 + ]; + let result = traverse_both( + [tip], + &odb, + Sorting::ByCommitTime(CommitTimeOrder::NewestFirst), + Parents::All, + [], + )?; + assert_eq!(result, expected_newest); + + // OldestFirst + let expected_oldest = [ + tip, + hex_to_id("a9c28710e058af4e5163699960234adb9fb2abc7"), // b2c2 + hex_to_id("b648f955b930ca95352fae6f22cb593ee0244b27"), // b2c1 + hex_to_id("ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83"), // c2 + hex_to_id("65d6af66f60b8e39fd1ba6a1423178831e764ec5"), // c1 + hex_to_id("0f6632a5a7d81417488b86692b729e49c1b73056"), // b1c2 + hex_to_id("77fd3c6832c0cd542f7a39f3af9250c3268db979"), // b1c1 + hex_to_id("2dce37be587e07caef8c4a5ab60b423b13a8536a"), // c3 + ]; + let result = traverse_both( + [tip], + &odb, + Sorting::ByCommitTime(CommitTimeOrder::OldestFirst), + Parents::All, + [], + )?; + assert_eq!(result, expected_oldest); + + Ok(()) +} diff --git a/gix-traverse/tests/traverse/commit/simple/hide.rs b/gix-traverse/tests/traverse/commit/simple/hide.rs new file mode 100644 index 00000000000..db59e7ed0d3 --- /dev/null +++ b/gix-traverse/tests/traverse/commit/simple/hide.rs @@ -0,0 +1,196 @@ +use super::*; +use crate::util::{fixture, git_rev_list}; + +#[test] +fn disjoint_hidden_and_interesting() -> crate::Result { + let (repo_dir, odb) = named_fixture("make_repos.sh", "disjoint_branches")?; + + insta::assert_snapshot!(git_graph(&repo_dir)?, @" + * e07cf1277ff7c43090f1acfc85a46039e7de1272 (HEAD -> disjoint) b3 + * 94cf3f3a4c782b672173423e7a4157a02957dd48 b2 + * 34e5ff5ce3d3ba9f0a00d11a7fad72551fff0861 b1 + * b5665181bf4c338ab16b10da0524d81b96aff209 (main) a3 + * f0230ce37b83d8e9f51ea6322ed7e8bd148d8e28 a2 + * 674aca0765b935ac5e7f7e9ab83af7f79272b5b0 a1 + "); + + let tip = hex_to_id("e07cf1277ff7c43090f1acfc85a46039e7de1272"); // b3 + let hidden = [hex_to_id("b5665181bf4c338ab16b10da0524d81b96aff209")]; // a3 + let expected = [ + tip, + hex_to_id("94cf3f3a4c782b672173423e7a4157a02957dd48"), // b2 + hex_to_id("34e5ff5ce3d3ba9f0a00d11a7fad72551fff0861"), // b1 + ]; + + for sorting in all_sortings() { + let result = traverse_both([tip], &odb, sorting, Parents::All, hidden)?; + assert_eq!(result, expected, "sorting = {sorting:?}"); + } + Ok(()) +} + +#[test] +fn all_hidden() -> crate::Result { + let (_repo_dir, odb) = named_fixture("make_repos.sh", "disjoint_branches")?; + let tips = [ + hex_to_id("e07cf1277ff7c43090f1acfc85a46039e7de1272"), // b3 + hex_to_id("b5665181bf4c338ab16b10da0524d81b96aff209"), // a3 + ]; + // The start positions are also declared hidden, so nothing should be visible. + let hidden = tips; + + for sorting in all_sortings() { + let result = traverse_both(tips, &odb, sorting, Parents::All, hidden)?; + assert!(result.is_empty(), "sorting = {sorting:?}"); + } + Ok(()) +} + +#[test] +fn some_hidden_and_all_hidden() -> crate::Result { + let (repo_dir, odb) = named_fixture("make_repos.sh", "simple")?; + + insta::assert_snapshot!(git_graph(&repo_dir)?, @r" + *-. f49838d84281c3988eeadd988d97dd358c9f9dc4 (HEAD -> main) merge + |\ \ + | | * 48e8dac19508f4238f06c8de2b10301ce64a641c (branch2) b2c2 + | | * cb6a6befc0a852ac74d74e0354e0f004af29cb79 b2c1 + | * | 66a309480201c4157b0eae86da69f2d606aadbe7 (branch1) b1c2 + | * | 80947acb398362d8236fcb8bf0f8a9dac640583f b1c1 + | |/ + * / 0edb95c0c0d9933d88f532ec08fcd405d0eee882 c5 + |/ + * 8cb5f13b66ce52a49399a2c49f537ee2b812369c c4 + * 33aa07785dd667c0196064e3be3c51dd9b4744ef c3 + * ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83 c2 + * 65d6af66f60b8e39fd1ba6a1423178831e764ec5 c1 + "); + + // Test: Hidden has to catch up with non-hidden + let tip_c2 = hex_to_id("ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83"); + let hidden_c5 = hex_to_id("0edb95c0c0d9933d88f532ec08fcd405d0eee882"); + + for sorting in all_sortings() { + let result = traverse_both([tip_c2], &odb, sorting, Parents::All, [hidden_c5])?; + assert!( + result.is_empty(), + "c2 is reachable from hidden c5, sorting = {sorting:?}" + ); + } + + // Test: merge tip with two branch tips hidden + let tip_merge = hex_to_id("f49838d84281c3988eeadd988d97dd358c9f9dc4"); + let hidden_branches = [ + hex_to_id("48e8dac19508f4238f06c8de2b10301ce64a641c"), // b2c2 + hex_to_id("66a309480201c4157b0eae86da69f2d606aadbe7"), // b1c2 + ]; + let expected = [ + tip_merge, + hex_to_id("0edb95c0c0d9933d88f532ec08fcd405d0eee882"), // c5 + ]; + + for sorting in all_sortings() { + let result = traverse_both([tip_merge], &odb, sorting, Parents::All, hidden_branches)?; + assert_eq!(result, expected, "sorting = {sorting:?}"); + } + + // Test: single-parent mode with hidden catching up + let tip_b1c1 = hex_to_id("80947acb398362d8236fcb8bf0f8a9dac640583f"); + let hidden_merge = hex_to_id("f49838d84281c3988eeadd988d97dd358c9f9dc4"); + + let result = traverse_both([tip_b1c1], &odb, Sorting::BreadthFirst, Parents::First, [hidden_merge])?; + assert!(result.is_empty(), "b1c1 is reachable from hidden merge"); + + Ok(()) +} + +fn hidden_bug_repo(name: &str) -> crate::Result<(std::path::PathBuf, gix_odb::Handle)> { + let dir = fixture("make_repo_for_hidden_bug.sh")?; + let repo_path = dir.join(name); + let odb = gix_odb::at(repo_path.join(".git").join("objects"))?; + Ok((repo_path, odb)) +} + +#[test] +fn hidden_tip_with_longer_path_to_shared_ancestor() -> crate::Result { + // Graph: + // A(tip) --> shared + // / + // H(hidden) --> X --> Y --> shared + // + // Expected: only A is returned (shared is reachable from H) + let (repo_path, odb) = hidden_bug_repo("long_hidden_path")?; + + insta::assert_snapshot!(git_graph(&repo_path)?, @" + * b6cf469d740a02645b7b9f7cdb98977a6cd7e5ab (HEAD -> main) A + | * 2955979fbddb1bddb9e1b1ca993789cacf612b18 (hidden_branch) H + | * ae431c4e51a81a1df4ac22a52c4e247734ee3c9d X + | * ab31ef4cacc50169f2b1d753c1e4efd55d570bbc Y + |/ + * f1543941113388f8a194164420fd7da96f73c2ce shared + "); + + let commits = parse_commit_names(&repo_path)?; + let tip_a = commits["A"]; + let hidden_h = commits["H"]; + let shared = commits["shared"]; + + let expected = vec![tip_a]; + + for sorting in all_sortings() { + let result = traverse([tip_a], &odb, sorting, Parents::All, [hidden_h])?; + assert_eq!( + result, expected, + "sorting = {sorting:?}: 'shared' ({shared}) should NOT be returned because it's \ + reachable from hidden tip H" + ); + } + + // Verify against git + let git_output = git_rev_list(&repo_path, &["main", "--not", "hidden_branch"])?; + assert_eq!(git_output, expected, "git rev-list should show only A"); + + Ok(()) +} + +#[test] +fn interesting_tip_with_longer_path_to_shared_ancestor() -> crate::Result { + // Graph: + // A(tip) --> B --> C --> D(shared) + // / + // H(hidden) --------->+ + // + // Expected: A, B, C are returned (D is reachable from H) + let (repo_path, odb) = hidden_bug_repo("long_interesting_path")?; + + insta::assert_snapshot!(git_graph(&repo_path)?, @" + * 8822f888affa916a2c945ef3b17447f29f8aabff (HEAD -> main) A + * 90f80e3c031e9149cfa631493663ffe52d645aab B + * 2f353d445c4c552eec8e84f0f6f73999d08a8073 C + | * 7e0cf8f62783a0eb1043fbe56d220308c3e0289e (hidden_branch) H + |/ + * 359b53df58a6e26b95e276a9d1c9e2b33a3b50bf D + "); + + let commits = parse_commit_names(&repo_path)?; + let tip_a = commits["A"]; + let hidden_h = commits["H"]; + let d = commits["D"]; + + let expected: Vec<_> = ["A", "B", "C"].iter().map(|name| commits[*name]).collect(); + + for sorting in all_sortings() { + let result = traverse([tip_a], &odb, sorting, Parents::All, [hidden_h])?; + assert_eq!( + result, expected, + "sorting = {sorting:?}: 'D' ({d}) should NOT be returned because it's \ + reachable from hidden tip H" + ); + } + + // Verify against git + let git_output = git_rev_list(&repo_path, &["main", "--not", "hidden_branch"])?; + assert_eq!(git_output, expected, "git rev-list should show A, B, C"); + + Ok(()) +} diff --git a/gix-traverse/tests/traverse/commit/simple/mod.rs b/gix-traverse/tests/traverse/commit/simple/mod.rs new file mode 100644 index 00000000000..9b67ac9b83e --- /dev/null +++ b/gix-traverse/tests/traverse/commit/simple/mod.rs @@ -0,0 +1,73 @@ +use crate::hex_to_id; +use crate::util::{commit_graph, git_graph, git_graph_with_time, named_fixture, parse_commit_names}; +use gix_hash::ObjectId; +use gix_traverse::commit::{simple::Sorting, Parents, Simple}; + +mod adjusted_dates; +mod different_date; +mod different_date_intermixed; +mod hide; +mod same_date; + +/// Run a simple traversal and collect the resulting commit IDs. +fn traverse( + tips: impl IntoIterator, + odb: &gix_odb::Handle, + sorting: Sorting, + parents: Parents, + hidden: impl IntoIterator, +) -> crate::Result> { + let graph = commit_graph(odb.store_ref()); + Simple::new(tips, odb) + .sorting(sorting)? + .parents(parents) + .hide(hidden)? + .commit_graph(graph) + .map(|res| res.map(|info| info.id)) + .collect::, _>>() + .map_err(Into::into) +} + +/// Run a traversal with both commit-graph enabled and disabled to ensure consistency. +fn traverse_both( + tips: impl IntoIterator + Clone, + odb: &gix_odb::Handle, + sorting: Sorting, + parents: Parents, + hidden: impl IntoIterator + Clone, +) -> crate::Result> { + // Without commit graph + let without_graph: Vec<_> = Simple::new(tips.clone(), odb) + .sorting(sorting)? + .parents(parents) + .hide(hidden.clone())? + .commit_graph(None) + .map(|res| res.map(|info| info.id)) + .collect::, _>>()?; + + // With commit graph + let graph = commit_graph(odb.store_ref()); + let with_graph: Vec<_> = Simple::new(tips, odb) + .sorting(sorting)? + .parents(parents) + .hide(hidden)? + .commit_graph(graph) + .map(|res| res.map(|info| info.id)) + .collect::, _>>()?; + + assert_eq!( + without_graph, with_graph, + "results must be consistent with and without commit-graph" + ); + Ok(with_graph) +} + +fn all_sortings() -> impl Iterator { + use gix_traverse::commit::simple::CommitTimeOrder; + [ + Sorting::BreadthFirst, + Sorting::ByCommitTime(CommitTimeOrder::NewestFirst), + Sorting::ByCommitTime(CommitTimeOrder::OldestFirst), + ] + .into_iter() +} diff --git a/gix-traverse/tests/traverse/commit/simple/same_date.rs b/gix-traverse/tests/traverse/commit/simple/same_date.rs new file mode 100644 index 00000000000..2fcd340ce4d --- /dev/null +++ b/gix-traverse/tests/traverse/commit/simple/same_date.rs @@ -0,0 +1,222 @@ +//! Same dates are somewhat special as they show how sorting-details on priority queues affects ordering +use super::*; +use crate::util::fixture; +use gix_hash::oid; +use gix_traverse::commit::simple::CommitTimeOrder; + +fn same_date_repo() -> crate::Result<(std::path::PathBuf, gix_odb::Handle)> { + let dir = fixture("make_traversal_repo_for_commits_same_date.sh")?; + let odb = gix_odb::at(dir.join(".git").join("objects"))?; + Ok((dir, odb)) +} + +#[test] +fn c4_breadth_first() -> crate::Result { + let (repo_dir, odb) = same_date_repo()?; + + insta::assert_snapshot!(git_graph(&repo_dir)?, @r" + * 01ec18a3ebf2855708ad3c9d244306bc1fae3e9b (HEAD -> main) m1b1 + |\ + | * ce2e8ffaa9608a26f7b21afc1db89cadb54fd353 (branch1) b1c2 + | * 9152eeee2328073cf23dcf8e90c949170b711659 b1c1 + * | efd9a841189668f1bab5b8ebade9cd0a1b139a37 c5 + |/ + * 9556057aee5abb06912922e9f26c46386a816822 c4 + * 17d78c64cef6c33a10a604573fd2c429e477fd63 c3 + * 9902e3c3e8f0c569b4ab295ddf473e6de763e1e7 c2 + * 134385f6d781b7e97062102c6a483440bfda2a03 c1 + "); + + let tip = hex_to_id("9556057aee5abb06912922e9f26c46386a816822"); // c4 + + let expected = [ + tip, + hex_to_id("17d78c64cef6c33a10a604573fd2c429e477fd63"), // c3 + hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7"), // c2 + hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"), // c1 + ]; + + let result = traverse_both([tip], &odb, Sorting::BreadthFirst, Parents::All, [])?; + assert_eq!(result, expected); + Ok(()) +} + +#[test] +fn head_breadth_first() -> crate::Result { + let (_repo_dir, odb) = same_date_repo()?; + // Graph shown in `c4_breadth_first` + let tip = hex_to_id("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. + let expected = [ + tip, + hex_to_id("efd9a841189668f1bab5b8ebade9cd0a1b139a37"), // c5 + hex_to_id("ce2e8ffaa9608a26f7b21afc1db89cadb54fd353"), // b1c2 + hex_to_id("9556057aee5abb06912922e9f26c46386a816822"), // c4 + hex_to_id("9152eeee2328073cf23dcf8e90c949170b711659"), // b1c1 + hex_to_id("17d78c64cef6c33a10a604573fd2c429e477fd63"), // c3 + hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7"), // c2 + hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"), // c1 + ]; + + let result = traverse_both([tip], &odb, Sorting::BreadthFirst, Parents::All, [])?; + assert_eq!(result, expected); + Ok(()) +} + +#[test] +fn head_date_order() -> crate::Result { + let (_repo_dir, odb) = same_date_repo()?; + // Graph shown in `c4_breadth_first` + let tip = hex_to_id("01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"); // m1b1 + + let expected = [ + tip, + hex_to_id("efd9a841189668f1bab5b8ebade9cd0a1b139a37"), // c5 + hex_to_id("ce2e8ffaa9608a26f7b21afc1db89cadb54fd353"), // b1c2 + hex_to_id("9556057aee5abb06912922e9f26c46386a816822"), // c4 + hex_to_id("9152eeee2328073cf23dcf8e90c949170b711659"), // b1c1 + hex_to_id("17d78c64cef6c33a10a604573fd2c429e477fd63"), // c3 + hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7"), // c2 + hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"), // c1 + ]; + + let result = traverse_both( + [tip], + &odb, + Sorting::ByCommitTime(CommitTimeOrder::NewestFirst), + Parents::All, + [], + )?; + assert_eq!(result, expected); + + let result = traverse_both( + [tip], + &odb, + Sorting::ByCommitTime(CommitTimeOrder::OldestFirst), + Parents::All, + [], + )?; + assert_eq!(result, expected); + + Ok(()) +} + +#[test] +fn head_first_parent_only_breadth_first() -> crate::Result { + let (_repo_dir, odb) = same_date_repo()?; + // Graph shown in `c4_breadth_first` + let tip = hex_to_id("01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"); // m1b1 + + let expected = [ + tip, + hex_to_id("efd9a841189668f1bab5b8ebade9cd0a1b139a37"), // c5 + hex_to_id("9556057aee5abb06912922e9f26c46386a816822"), // c4 + hex_to_id("17d78c64cef6c33a10a604573fd2c429e477fd63"), // c3 + hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7"), // c2 + hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"), // c1 + ]; + + let result = traverse_both([tip], &odb, Sorting::BreadthFirst, Parents::First, [])?; + assert_eq!(result, expected); + Ok(()) +} + +#[test] +fn head_c4_breadth_first() -> crate::Result { + let (_repo_dir, odb) = same_date_repo()?; + // Graph shown in `c4_breadth_first` + let tips = [ + hex_to_id("01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"), // m1b1 + hex_to_id("9556057aee5abb06912922e9f26c46386a816822"), // c4 + ]; + + let expected = [ + tips[0], + tips[1], + hex_to_id("efd9a841189668f1bab5b8ebade9cd0a1b139a37"), // c5 + hex_to_id("ce2e8ffaa9608a26f7b21afc1db89cadb54fd353"), // b1c2 + hex_to_id("17d78c64cef6c33a10a604573fd2c429e477fd63"), // c3 + hex_to_id("9152eeee2328073cf23dcf8e90c949170b711659"), // b1c1 + hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7"), // c2 + hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"), // c1 + ]; + + let result = traverse_both(tips, &odb, Sorting::BreadthFirst, Parents::All, [])?; + assert_eq!(result, expected); + Ok(()) +} + +#[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. + let (_repo_dir, odb) = same_date_repo()?; + // Graph shown in `c4_breadth_first` + let tip = hex_to_id("01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"); // m1b1 + let filter_out = hex_to_id("9152eeee2328073cf23dcf8e90c949170b711659"); // b1c1 + + let expected = [ + tip, + hex_to_id("efd9a841189668f1bab5b8ebade9cd0a1b139a37"), // c5 + hex_to_id("ce2e8ffaa9608a26f7b21afc1db89cadb54fd353"), // b1c2 + hex_to_id("9556057aee5abb06912922e9f26c46386a816822"), // c4 + hex_to_id("17d78c64cef6c33a10a604573fd2c429e477fd63"), // c3 + hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7"), // c2 + hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"), // c1 + ]; + + let graph = commit_graph(odb.store_ref()); + let result: Vec<_> = Simple::filtered([tip], &odb, move |id: &oid| id != filter_out) + .sorting(Sorting::BreadthFirst)? + .parents(Parents::All) + .hide([])? + .commit_graph(graph) + .map(|res| res.map(|info| info.id)) + .collect::, _>>()?; + + assert_eq!(result, expected); + Ok(()) +} + +#[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 (_repo_dir, odb) = same_date_repo()?; + // Graph shown in `c4_breadth_first` + let tip = hex_to_id("01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"); // m1b1 + let filter_out = hex_to_id("9556057aee5abb06912922e9f26c46386a816822"); // c4 + + let expected = [ + tip, + hex_to_id("efd9a841189668f1bab5b8ebade9cd0a1b139a37"), // c5 + hex_to_id("ce2e8ffaa9608a26f7b21afc1db89cadb54fd353"), // b1c2 + hex_to_id("9152eeee2328073cf23dcf8e90c949170b711659"), // b1c1 + ]; + + let mut seen = false; + let graph = commit_graph(odb.store_ref()); + let result: Vec<_> = Simple::filtered([tip], &odb, move |id: &oid| { + if id == filter_out { + assert!(!seen, "predicate should only be called once for c4"); + seen = true; + false + } else { + true + } + }) + .sorting(Sorting::BreadthFirst)? + .parents(Parents::All) + .hide([])? + .commit_graph(graph) + .map(|res| res.map(|info| info.id)) + .collect::, _>>()?; + + assert_eq!(result, expected); + Ok(()) +} diff --git a/gix-traverse/tests/traverse/commit/topo.rs b/gix-traverse/tests/traverse/commit/topo.rs index 0b9ffe6cfb6..7897b610c7c 100644 --- a/gix-traverse/tests/traverse/commit/topo.rs +++ b/gix-traverse/tests/traverse/commit/topo.rs @@ -1,373 +1,367 @@ -use std::path::PathBuf; - +use crate::hex_to_id; +use crate::util::{commit_graph, fixture, fixture_odb}; 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, +fn odb() -> crate::Result { + fixture_odb("make_repo_for_topo.sh") } -/// 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::, _>>()?; +fn fixture_dir() -> crate::Result { + fixture("make_repo_for_topo.sh") +} - assert_eq!(oids, expected); - } - Ok(()) - } +/// Run a topo traversal with both commit-graph enabled and disabled to ensure consistency. +fn traverse_both( + tips: impl IntoIterator + Clone, + ends: impl IntoIterator + Clone, + odb: &gix_odb::Handle, + sorting: topo::Sorting, + parents: Parents, +) -> crate::Result> { + // Without commit graph + let without_graph: Vec<_> = topo::Builder::from_iters(odb, tips.clone(), Some(ends.clone())) + .sorting(sorting) + .with_commit_graph(None) + .parents(parents) + .build()? + .map(|res| res.map(|info| info.id)) + .collect::, _>>()?; + + // With commit graph + let graph = commit_graph(odb.store_ref()); + let with_graph: Vec<_> = topo::Builder::from_iters(odb, tips, Some(ends)) + .sorting(sorting) + .with_commit_graph(graph) + .parents(parents) + .build()? + .map(|res| res.map(|info| info.id)) + .collect::, _>>()?; + + assert_eq!( + without_graph, with_graph, + "results must be consistent with and without commit-graph" + ); + Ok(with_graph) } -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)) - } +/// Run a topo traversal with a predicate filter. +fn traverse_with_predicate( + tips: impl IntoIterator + Clone, + ends: impl IntoIterator + Clone, + odb: &gix_odb::Handle, + sorting: topo::Sorting, + parents: Parents, + predicate: impl FnMut(&oid) -> bool + Clone, +) -> crate::Result> { + // Without commit graph + let without_graph: Vec<_> = topo::Builder::from_iters(odb, tips.clone(), Some(ends.clone())) + .sorting(sorting) + .with_commit_graph(None) + .parents(parents) + .with_predicate(predicate.clone()) + .build()? + .map(|res| res.map(|info| info.id)) + .collect::, _>>()?; + + // With commit graph + let graph = commit_graph(odb.store_ref()); + let with_graph: Vec<_> = topo::Builder::from_iters(odb, tips, Some(ends)) + .sorting(sorting) + .with_commit_graph(graph) + .parents(parents) + .with_predicate(predicate) + .build()? + .map(|res| res.map(|info| info.id)) + .collect::, _>>()?; + + assert_eq!( + without_graph, with_graph, + "results must be consistent with and without commit-graph" + ); + Ok(with_graph) +} - 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") - } +/// Read baseline file and parse expected commit hashes. +fn read_baseline(fixture_dir: &std::path::Path, name: &str) -> crate::Result> { + let buf = std::fs::read(fixture_dir.join(format!("{name}.baseline")))?; + Ok(buf.lines().map(|s| s.to_str().unwrap().to_string()).collect()) } mod basic { - use gix_traverse::commit::topo; - - use super::TraversalAssertion; - use crate::hex_to_id; + use super::*; #[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"); + let odb = odb()?; + let tip = hex_to_id("62ed296d9986f50477e9f7b7e81cd0258939a43d"); + + let expected = [ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "3be0c4c793c634c8fd95054345d4935d10a0879a", + "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", + "302a5d0530ec688c241f32c2f2b61b964dd17bee", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + "f1cce1b5c7efcdfa106e95caa6c45a2cae48a481", + "945d8a360915631ad545e0cf04630d86d3d4eaa1", + "a863c02247a6c5ba32dff5224459f52aa7f77f7b", + "2f291881edfb0597493a52d26ea09dd7340ce507", + "9c46b8765703273feb10a2ebd810e70b8e2ca44a", + "fb3e21cf45b04b617011d2b30973f3e5ce60d0cd", + ] + .map(hex_to_id); + + let result = traverse_both([tip], [], &odb, topo::Sorting::TopoOrder, Parents::All)?; + assert_eq!(result, expected); + + // Verify against baseline + let baseline = read_baseline(&fixture_dir()?, "all-commits")?; + let expected_strs: Vec<_> = expected.iter().map(std::string::ToString::to_string).collect(); + assert_eq!(expected_strs, baseline, "Baseline must match the expectation"); + 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() + let odb = odb()?; + let tip = hex_to_id("62ed296d9986f50477e9f7b7e81cd0258939a43d"); + let end = hex_to_id("f1cce1b5c7efcdfa106e95caa6c45a2cae48a481"); + + let expected = [ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "3be0c4c793c634c8fd95054345d4935d10a0879a", + "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", + "302a5d0530ec688c241f32c2f2b61b964dd17bee", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + ] + .map(hex_to_id); + + let result = traverse_both([tip], [end], &odb, topo::Sorting::TopoOrder, Parents::All)?; + assert_eq!(result, expected); + Ok(()) } #[test] fn empty_range() -> crate::Result { - TraversalAssertion::new( - &["f1cce1b5c7efcdfa106e95caa6c45a2cae48a481"], - &["eeab3243aad67bc838fc4425f759453bf0b47785"], - &[], - ) - .with_sorting(topo::Sorting::TopoOrder) - .check() + let odb = odb()?; + let tip = hex_to_id("f1cce1b5c7efcdfa106e95caa6c45a2cae48a481"); + let end = hex_to_id("eeab3243aad67bc838fc4425f759453bf0b47785"); + + let result = traverse_both([tip], [end], &odb, topo::Sorting::TopoOrder, Parents::All)?; + assert!(result.is_empty()); + Ok(()) } #[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() + let odb = odb()?; + let tips = [ + hex_to_id("d09384f312b03e4a1413160739805ff25e8fe99d"), + hex_to_id("3be0c4c793c634c8fd95054345d4935d10a0879a"), + ]; + let ends = [ + hex_to_id("1a27cb1a26c9faed9f0d1975326fe51123ab01ed"), + hex_to_id("22fbc169eeca3c9678fc7028aa80fad5ef49019f"), + ]; + + let expected = [ + "3be0c4c793c634c8fd95054345d4935d10a0879a", + "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", + "302a5d0530ec688c241f32c2f2b61b964dd17bee", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + ] + .map(hex_to_id); + + let result = traverse_both(tips, ends, &odb, topo::Sorting::TopoOrder, Parents::All)?; + assert_eq!(result, expected); + Ok(()) } #[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")) + let odb = odb()?; + let tip = hex_to_id("62ed296d9986f50477e9f7b7e81cd0258939a43d"); + let filter_out = hex_to_id("eeab3243aad67bc838fc4425f759453bf0b47785"); + + let expected = [ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "3be0c4c793c634c8fd95054345d4935d10a0879a", + "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", + "302a5d0530ec688c241f32c2f2b61b964dd17bee", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + "f1cce1b5c7efcdfa106e95caa6c45a2cae48a481", + "945d8a360915631ad545e0cf04630d86d3d4eaa1", + "a863c02247a6c5ba32dff5224459f52aa7f77f7b", + "2f291881edfb0597493a52d26ea09dd7340ce507", + "9c46b8765703273feb10a2ebd810e70b8e2ca44a", + "fb3e21cf45b04b617011d2b30973f3e5ce60d0cd", + ] + .map(hex_to_id); + + let result = traverse_with_predicate([tip], [], &odb, topo::Sorting::TopoOrder, Parents::All, move |oid| { + oid != filter_out + })?; + assert_eq!(result, expected); + Ok(()) } #[test] fn end_along_first_parent() -> crate::Result { - TraversalAssertion::new( - &["d09384f312b03e4a1413160739805ff25e8fe99d"], - &["33eb18340e4eaae3e3dcf80222b02f161cd3f966"], - &[ - "d09384f312b03e4a1413160739805ff25e8fe99d", - "22fbc169eeca3c9678fc7028aa80fad5ef49019f", - "eeab3243aad67bc838fc4425f759453bf0b47785", - "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", - ], - ) - .with_sorting(topo::Sorting::TopoOrder) - .check() + let odb = odb()?; + let tip = hex_to_id("d09384f312b03e4a1413160739805ff25e8fe99d"); + let end = hex_to_id("33eb18340e4eaae3e3dcf80222b02f161cd3f966"); + + let expected = [ + "d09384f312b03e4a1413160739805ff25e8fe99d", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + ] + .map(hex_to_id); + + let result = traverse_both([tip], [end], &odb, topo::Sorting::TopoOrder, Parents::All)?; + assert_eq!(result, expected); + Ok(()) } } mod first_parent { - use gix_traverse::commit::{topo, Parents}; - - use super::TraversalAssertion; + use super::*; #[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"); + let odb = odb()?; + let tip = hex_to_id("62ed296d9986f50477e9f7b7e81cd0258939a43d"); + + let expected = [ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + "f1cce1b5c7efcdfa106e95caa6c45a2cae48a481", + "945d8a360915631ad545e0cf04630d86d3d4eaa1", + "a863c02247a6c5ba32dff5224459f52aa7f77f7b", + "2f291881edfb0597493a52d26ea09dd7340ce507", + "9c46b8765703273feb10a2ebd810e70b8e2ca44a", + "fb3e21cf45b04b617011d2b30973f3e5ce60d0cd", + ] + .map(hex_to_id); + + let result = traverse_both([tip], [], &odb, topo::Sorting::TopoOrder, Parents::First)?; + assert_eq!(result, expected); + + // Verify against baseline + let baseline = read_baseline(&fixture_dir()?, "first-parent")?; + let expected_strs: Vec<_> = expected.iter().map(std::string::ToString::to_string).collect(); + assert_eq!(expected_strs, baseline, "Baseline must match the expectation"); + 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() + let odb = odb()?; + let tip = hex_to_id("62ed296d9986f50477e9f7b7e81cd0258939a43d"); + let end = hex_to_id("f1cce1b5c7efcdfa106e95caa6c45a2cae48a481"); + + let expected = [ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + ] + .map(hex_to_id); + + let result = traverse_both([tip], [end], &odb, topo::Sorting::TopoOrder, Parents::First)?; + assert_eq!(result, expected); + Ok(()) } #[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() + let odb = odb()?; + let tip = hex_to_id("62ed296d9986f50477e9f7b7e81cd0258939a43d"); + let end = hex_to_id("3be0c4c793c634c8fd95054345d4935d10a0879a"); + + let expected = [ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + ] + .map(hex_to_id); + + let result = traverse_both([tip], [end], &odb, topo::Sorting::TopoOrder, Parents::First)?; + assert_eq!(result, expected); + Ok(()) } } mod date_order { - use gix_traverse::commit::topo; - - use super::TraversalAssertion; + use super::*; #[test] fn with_ends() -> crate::Result { - let mut assertion = 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", - ], - ); - assertion.with_sorting(topo::Sorting::DateOrder).check()?; - assertion.assert_baseline("date-order"); + let odb = odb()?; + // Same tip and end as basic::one_end() but the order should be different. + let tip = hex_to_id("62ed296d9986f50477e9f7b7e81cd0258939a43d"); + let end = hex_to_id("f1cce1b5c7efcdfa106e95caa6c45a2cae48a481"); + + let expected = [ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "3be0c4c793c634c8fd95054345d4935d10a0879a", + "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", + "302a5d0530ec688c241f32c2f2b61b964dd17bee", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + ] + .map(hex_to_id); + + let result = traverse_both([tip], [end], &odb, topo::Sorting::DateOrder, Parents::All)?; + assert_eq!(result, expected); + + // Verify against baseline + let baseline = read_baseline(&fixture_dir()?, "date-order")?; + let expected_strs: Vec<_> = expected.iter().map(std::string::ToString::to_string).collect(); + assert_eq!(expected_strs, baseline, "Baseline must match the expectation"); + Ok(()) } } diff --git a/gix-traverse/tests/traverse/main.rs b/gix-traverse/tests/traverse/main.rs index 81ecb3be5b0..fd5fd2e474d 100644 --- a/gix-traverse/tests/traverse/main.rs +++ b/gix-traverse/tests/traverse/main.rs @@ -1,8 +1,5 @@ -use gix_testtools::Result; - -fn hex_to_id(hex: &str) -> gix_hash::ObjectId { - gix_hash::ObjectId::from_hex(hex.as_bytes()).expect("40 bytes hex") -} +mod util; +pub use util::{hex_to_id, Result}; mod commit; mod tree; diff --git a/gix-traverse/tests/traverse/tree.rs b/gix-traverse/tests/traverse/tree.rs index 003ecbf3b32..93ba1e8eae9 100644 --- a/gix-traverse/tests/traverse/tree.rs +++ b/gix-traverse/tests/traverse/tree.rs @@ -1,25 +1,20 @@ -fn db() -> crate::Result { - named_db("make_traversal_repo_for_trees.sh") -} +use crate::hex_to_id; +use crate::util::fixture_odb; -fn named_db(name: &str) -> crate::Result { - let dir = gix_testtools::scripted_fixture_read_only_standalone(name)?; - let db = gix_odb::at(dir.join(".git").join("objects"))?; - Ok(db) +fn odb() -> crate::Result { + fixture_odb("make_traversal_repo_for_trees.sh") } mod depthfirst { use gix_object::FindExt; use gix_traverse::{tree, tree::recorder::Location}; - use crate::{ - hex_to_id, - tree::{db, named_db}, - }; + use super::*; + use crate::util::fixture_odb; #[test] fn full_path_and_filename() -> crate::Result { - let db = db()?; + let db = odb()?; let mut state = gix_traverse::tree::depthfirst::State::default(); let mut buf = state.pop_buf(); let mut recorder = tree::Recorder::default(); @@ -165,7 +160,7 @@ mod depthfirst { #[test] fn more_difficult_fixture() -> crate::Result { - let db = named_db("make_traversal_repo_for_trees_depthfirst.sh")?; + let db = fixture_odb("make_traversal_repo_for_trees_depthfirst.sh")?; let mut state = gix_traverse::tree::depthfirst::State::default(); let mut buf = state.pop_buf(); let mut recorder = tree::Recorder::default(); @@ -237,11 +232,11 @@ mod breadthfirst { use gix_odb::pack::FindExt; use gix_traverse::{tree, tree::recorder::Location}; - use crate::{hex_to_id, tree::db}; + use super::*; #[test] fn full_path() -> crate::Result { - let db = db()?; + let db = odb()?; let mut buf = Vec::new(); let mut buf2 = Vec::new(); let mut commit = db @@ -329,7 +324,7 @@ mod breadthfirst { #[test] fn filename_only() -> crate::Result<()> { - let db = db()?; + let db = odb()?; let mut buf = Vec::new(); let mut buf2 = Vec::new(); let mut commit = db @@ -356,7 +351,7 @@ mod breadthfirst { #[test] fn no_location() -> crate::Result<()> { - let db = db()?; + let db = odb()?; let mut buf = Vec::new(); let mut buf2 = Vec::new(); let mut commit = db diff --git a/gix-traverse/tests/traverse/util.rs b/gix-traverse/tests/traverse/util.rs new file mode 100644 index 00000000000..1cb911c4d45 --- /dev/null +++ b/gix-traverse/tests/traverse/util.rs @@ -0,0 +1,93 @@ +use gix_hash::ObjectId; +use std::path::PathBuf; + +pub use gix_testtools::Result; + +/// Convert a hexadecimal hash into its corresponding `ObjectId` or _panic_. +pub fn hex_to_id(hex: &str) -> ObjectId { + ObjectId::from_hex(hex.as_bytes()).expect("40 bytes hex") +} + +/// Get the path to a fixture directory from a script that creates a single repository. +pub fn fixture(script_name: &str) -> Result { + gix_testtools::scripted_fixture_read_only_standalone(script_name) +} + +/// Get an object database handle from a fixture script that creates a single repository. +pub fn fixture_odb(script_name: &str) -> Result { + let dir = fixture(script_name)?; + Ok(gix_odb::at(dir.join(".git").join("objects"))?) +} + +/// Get a fixture path and object database for a named sub-repository within a fixture. +pub fn named_fixture(script_name: &str, repo_name: &str) -> Result<(PathBuf, gix_odb::Handle)> { + let dir = fixture(script_name)?; + let repo_dir = dir.join(repo_name); + let odb = gix_odb::at(repo_dir.join(".git").join("objects"))?; + Ok((repo_dir, odb)) +} + +/// Load a commit graph if available for the given object store. +pub fn commit_graph(store: &gix_odb::Store) -> Option { + gix_commitgraph::at(store.path().join("info")).ok() +} + +/// Execute `git log --oneline --graph --decorate --all` in the given repository +/// and return the output as a string. Useful for snapshot testing. +pub fn git_graph(repo_dir: impl AsRef) -> Result { + git_graph_internal(repo_dir, false) +} + +/// Like `git_graph`, but includes commit timestamps (Unix epoch seconds). +/// Use this for tests where commit ordering depends on time. +pub fn git_graph_with_time(repo_dir: impl AsRef) -> Result { + git_graph_internal(repo_dir, true) +} + +fn git_graph_internal(repo_dir: impl AsRef, with_time: bool) -> Result { + use gix_object::bstr::{ByteSlice, ByteVec}; + let format = if with_time { + "--pretty=format:%H %ct%d %s" + } else { + "--pretty=format:%H %d %s" + }; + let out = std::process::Command::new(gix_path::env::exe_invocation()) + .current_dir(repo_dir) + .args(["log", "--oneline", "--graph", "--decorate", "--all", format]) + .output()?; + if !out.status.success() { + return Err(format!("git log failed: {err}", err = out.stderr.to_str_lossy()).into()); + } + Ok(out.stdout.into_string_lossy()) +} + +/// Parse commit names to IDs from git log output. +/// Returns a map of commit message (first word) to ObjectId. +pub fn parse_commit_names(repo_path: &std::path::Path) -> Result> { + let output = std::process::Command::new("git") + .current_dir(repo_path) + .args(["log", "--all", "--format=%H %s"]) + .output()?; + let mut commits = std::collections::HashMap::new(); + for line in String::from_utf8_lossy(&output.stdout).lines() { + let mut parts = line.split_whitespace(); + if let (Some(hash), Some(name)) = (parts.next(), parts.next()) { + commits.insert(name.to_string(), hex_to_id(hash)); + } + } + Ok(commits) +} + +/// Run `git rev-list` with the given arguments and return the resulting commit IDs. +/// Useful for verifying traversal results against git's baseline behavior. +pub fn git_rev_list(repo_path: &std::path::Path, args: &[&str]) -> Result> { + let output = std::process::Command::new("git") + .current_dir(repo_path) + .arg("rev-list") + .args(args) + .output()?; + Ok(String::from_utf8_lossy(&output.stdout) + .lines() + .map(|s| hex_to_id(s.trim())) + .collect()) +}