From 1617444fa1b0434c421732e9df48fe6b1e67be72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 05:02:47 +0000 Subject: [PATCH 1/4] fix(gix-traverse): use graph painting for hide() to ensure correct behavior The hide() function now fully traverses all hidden tips and marks all reachable commits as Hidden before returning. This "graph painting" approach ensures correct behavior regardless of graph topology or traversal order, matching git's behavior. Previously, hidden commits were traversed lazily during iteration, interleaved with interesting commits. While this worked in most cases, the new approach is simpler and more robust. Added tests to verify correct behavior when: - Hidden tip has longer path to shared ancestor - Interesting tip has longer path to shared ancestor Co-authored-by: Byron <63622+Byron@users.noreply.github.com> --- gix-traverse/src/commit/simple.rs | 96 ++++++++---- .../fixtures/make_repo_for_hidden_bug.sh | 72 +++++++++ gix-traverse/tests/traverse/commit/simple.rs | 137 ++++++++++++++++++ 3 files changed, 274 insertions(+), 31 deletions(-) create mode 100755 gix-traverse/tests/fixtures/make_repo_for_hidden_bug.sh diff --git a/gix-traverse/src/commit/simple.rs b/gix-traverse/src/commit/simple.rs index 7b851765bf..4e603364b3 100644 --- a/gix-traverse/src/commit/simple.rs +++ b/gix-traverse/src/commit/simple.rs @@ -212,45 +212,79 @@ 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 "graph painting" approach ensures correct behavior regardless + /// of graph topology or traversal order, matching git's `rev-list --not` behavior. /// /// Note that hidden objects are expected to exist. 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 hidden_queue: VecDeque = VecDeque::new(); + + for id_to_ignore in hidden_tips { + if self.state.seen.insert(id_to_ignore, CommitState::Hidden).is_none() { + hidden_queue.push_back(id_to_ignore); + } + } + + // Process all hidden commits and their ancestors + while let Some(id) = hidden_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) { + 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() { + hidden_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() { + hidden_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 = Some(VecDeque::new()); + + // 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/make_repo_for_hidden_bug.sh b/gix-traverse/tests/fixtures/make_repo_for_hidden_bug.sh new file mode 100755 index 0000000000..748444a629 --- /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 index 3529f10198..891c6997fb 100644 --- a/gix-traverse/tests/traverse/commit/simple.rs +++ b/gix-traverse/tests/traverse/commit/simple.rs @@ -271,6 +271,143 @@ mod hide { } } +mod hide_with_graph_painting { + //! These tests verify that the hide functionality works correctly regardless of + //! the relative path lengths between interesting and hidden tips to shared ancestors. + //! + //! The implementation must ensure all commits reachable from hidden tips are properly + //! excluded, regardless of traversal order. + + use crate::hex_to_id; + use gix_traverse::commit::simple::{CommitTimeOrder, Sorting}; + use gix_traverse::commit::{Parents, Simple}; + use std::collections::HashMap; + + /// Parse commit names to IDs from git log output + fn parse_commits(repo_path: &std::path::Path) -> crate::Result> { + let output = std::process::Command::new("git") + .current_dir(repo_path) + .args(["log", "--all", "--format=%H %s"]) + .output()?; + let mut commits = 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) + } + + fn all_sortings() -> impl Iterator { + [ + Sorting::BreadthFirst, + Sorting::ByCommitTime(CommitTimeOrder::NewestFirst), + Sorting::ByCommitTime(CommitTimeOrder::OldestFirst), + ] + .into_iter() + } + + #[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 dir = gix_testtools::scripted_fixture_read_only_standalone("make_repo_for_hidden_bug.sh")?; + let repo_path = dir.join("long_hidden_path"); + let store = gix_odb::at(repo_path.join(".git").join("objects"))?; + + let commits = parse_commits(&repo_path)?; + let tip_a = commits["A"]; + let hidden_h = commits["H"]; + let shared = commits["shared"]; + + // Expected: only A (shared is reachable from H) + let expected = vec![tip_a]; + + for sorting in all_sortings() { + let oids: Vec<_> = Simple::new(Some(tip_a), &store) + .sorting(sorting)? + .parents(Parents::All) + .hide(Some(hidden_h))? + .map(|res| res.map(|info| info.id)) + .collect::, _>>()?; + + assert_eq!( + oids, expected, + "sorting = {sorting:?}: 'shared' ({shared}) should NOT be returned because it's \ + reachable from hidden tip H. Got: {oids:?}" + ); + } + + // Verify against git + let output = std::process::Command::new("git") + .current_dir(&repo_path) + .args(["rev-list", "main", "--not", "hidden_branch"]) + .output()?; + let git_output: Vec<_> = String::from_utf8_lossy(&output.stdout) + .lines() + .map(|s| hex_to_id(s.trim())) + .collect(); + 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 dir = gix_testtools::scripted_fixture_read_only_standalone("make_repo_for_hidden_bug.sh")?; + let repo_path = dir.join("long_interesting_path"); + let store = gix_odb::at(repo_path.join(".git").join("objects"))?; + + let commits = parse_commits(&repo_path)?; + let tip_a = commits["A"]; + let hidden_h = commits["H"]; + let d = commits["D"]; + + // Expected: A, B, C (D is reachable from H) + let expected_commits = vec!["A", "B", "C"]; + let expected: Vec<_> = expected_commits.iter().map(|name| commits[*name]).collect(); + + for sorting in all_sortings() { + let oids: Vec<_> = Simple::new(Some(tip_a), &store) + .sorting(sorting)? + .parents(Parents::All) + .hide(Some(hidden_h))? + .map(|res| res.map(|info| info.id)) + .collect::, _>>()?; + + assert_eq!( + oids, expected, + "sorting = {sorting:?}: 'D' ({d}) should NOT be returned because it's \ + reachable from hidden tip H. Got: {oids:?}" + ); + } + + // Verify against git + let output = std::process::Command::new("git") + .current_dir(&repo_path) + .args(["rev-list", "main", "--not", "hidden_branch"]) + .output()?; + let git_output: Vec<_> = String::from_utf8_lossy(&output.stdout) + .lines() + .map(|s| hex_to_id(s.trim())) + .collect(); + assert_eq!(git_output, expected, "git rev-list should show A, B, C"); + + Ok(()) + } +} + mod different_date_intermixed { use gix_traverse::commit::simple::{CommitTimeOrder, Sorting}; From 90bbd956eb13d0cba46588525e3a19e6b7827bec Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 3 Feb 2026 14:59:17 +0100 Subject: [PATCH 2/4] Improve test setup for `gix-traverse` Co-authored-by: Claude --- gix-traverse/src/commit/simple.rs | 6 +- .../make_repo_for_hidden_bug.tar | Bin 0 -> 132096 bytes gix-traverse/tests/traverse/commit/simple.rs | 1224 ++++++++--------- gix-traverse/tests/traverse/commit/topo.rs | 614 ++++----- gix-traverse/tests/traverse/main.rs | 7 +- gix-traverse/tests/traverse/tree.rs | 29 +- gix-traverse/tests/traverse/util.rs | 77 ++ 7 files changed, 989 insertions(+), 968 deletions(-) create mode 100644 gix-traverse/tests/fixtures/generated-archives/make_repo_for_hidden_bug.tar create mode 100644 gix-traverse/tests/traverse/util.rs diff --git a/gix-traverse/src/commit/simple.rs b/gix-traverse/src/commit/simple.rs index 4e603364b3..45dd09f8d7 100644 --- a/gix-traverse/src/commit/simple.rs +++ b/gix-traverse/src/commit/simple.rs @@ -276,14 +276,16 @@ mod init { // 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 = Some(VecDeque::new()); // Remove any hidden commits from the interesting queues - self.state.next.retain(|(id, _)| !self.state.seen.get(id).is_some_and(CommitState::is_hidden)); + 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 0000000000000000000000000000000000000000..069be58cc5c5bd1dd7a15ba46449f7e4b1980722 GIT binary patch literal 132096 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 HDF*(3I7_ul literal 0 HcmV?d00001 diff --git a/gix-traverse/tests/traverse/commit/simple.rs b/gix-traverse/tests/traverse/commit/simple.rs index 891c6997fb..09869d5b56 100644 --- a/gix-traverse/tests/traverse/commit/simple.rs +++ b/gix-traverse/tests/traverse/commit/simple.rs @@ -1,157 +1,79 @@ 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], +use crate::util::{commit_graph, git_graph, named_fixture, named_fixture_odb, parse_commit_names}; +use gix_hash::ObjectId; +use gix_traverse::commit::{simple::Sorting, Parents, Simple}; + +/// 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) } -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)) - } +/// 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) } -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(()) - } +fn all_sortings() -> impl Iterator { + use gix_traverse::commit::simple::CommitTimeOrder; + [ + Sorting::BreadthFirst, + Sorting::ByCommitTime(CommitTimeOrder::NewestFirst), + Sorting::ByCommitTime(CommitTimeOrder::OldestFirst), + ] + .into_iter() } 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() - } + use super::*; #[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" + let (repo_dir, odb) = named_fixture("make_repos.sh", "disjoint_branches")?; + + insta::assert_snapshot!(git_graph(&repo_dir)?, @r" * e07cf1277ff7c43090f1acfc85a46039e7de1272 (HEAD -> disjoint) b3 * 94cf3f3a4c782b672173423e7a4157a02957dd48 b2 * 34e5ff5ce3d3ba9f0a00d11a7fad72551fff0861 b1 @@ -160,28 +82,26 @@ mod hide { * 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() { - assertion - .with_hidden(&["b5665181bf4c338ab16b10da0524d81b96aff209" /* a3 */]) - .with_sorting(sorting) - .check()?; + let result = traverse_both([tip], &odb, sorting, Parents::All, hidden.clone())?; + assert_eq!(result, expected, "sorting = {sorting:?}"); } 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" + let (repo_dir, odb) = named_fixture("make_repos.sh", "disjoint_branches")?; + + insta::assert_snapshot!(git_graph(&repo_dir)?, @r" * e07cf1277ff7c43090f1acfc85a46039e7de1272 (HEAD -> disjoint) b3 * 94cf3f3a4c782b672173423e7a4157a02957dd48 b2 * 34e5ff5ce3d3ba9f0a00d11a7fad72551fff0861 b1 @@ -190,30 +110,25 @@ mod hide { * 674aca0765b935ac5e7f7e9ab83af7f79272b5b0 a1 "); + 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.clone(); + for sorting in all_sortings() { - assertion - .with_hidden(&[ - "e07cf1277ff7c43090f1acfc85a46039e7de1272", /* b3 */ - "b5665181bf4c338ab16b10da0524d81b96aff209", /* a3 */ - ]) - .with_sorting(sorting) - .expected_without_tips() - .check()?; + let result = traverse_both(tips.clone(), &odb, sorting, Parents::All, hidden.clone())?; + assert!(result.is_empty(), "sorting = {sorting:?}"); } 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" + 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 @@ -229,84 +144,54 @@ mod hide { * 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() { - assertion - .with_hidden(&["0edb95c0c0d9933d88f532ec08fcd405d0eee882" /* c5 */]) - .expected_without_tips() - .with_sorting(sorting) - .check()?; + let result = traverse_both([tip_c2], &odb, sorting, Parents::All, [hidden_c5])?; + assert!( + result.is_empty(), + "c2 is reachable from hidden c5, sorting = {sorting:?}" + ); } - let mut assertion = TraversalAssertion::new_at( - "make_repos.sh", - "simple", - &["f49838d84281c3988eeadd988d97dd358c9f9dc4"], /* merge */ - &["0edb95c0c0d9933d88f532ec08fcd405d0eee882" /* c5 */], - ); + + // 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() { - assertion - .with_hidden(&[ - "48e8dac19508f4238f06c8de2b10301ce64a641c", /* b2c2 */ - "66a309480201c4157b0eae86da69f2d606aadbe7", /* b1c2 */ - ]) - .with_sorting(sorting) - .check()?; + let result = traverse_both([tip_merge], &odb, sorting, Parents::All, hidden_branches.clone())?; + assert_eq!(result, expected, "sorting = {sorting:?}"); } - 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()?; + // 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(()) } } mod hide_with_graph_painting { - //! These tests verify that the hide functionality works correctly regardless of + //! These tests verify th //! the relative path lengths between interesting and hidden tips to shared ancestors. //! //! The implementation must ensure all commits reachable from hidden tips are properly //! excluded, regardless of traversal order. - use crate::hex_to_id; - use gix_traverse::commit::simple::{CommitTimeOrder, Sorting}; - use gix_traverse::commit::{Parents, Simple}; - use std::collections::HashMap; - - /// Parse commit names to IDs from git log output - fn parse_commits(repo_path: &std::path::Path) -> crate::Result> { - let output = std::process::Command::new("git") - .current_dir(repo_path) - .args(["log", "--all", "--format=%H %s"]) - .output()?; - let mut commits = 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) - } - - fn all_sortings() -> impl Iterator { - [ - Sorting::BreadthFirst, - Sorting::ByCommitTime(CommitTimeOrder::NewestFirst), - Sorting::ByCommitTime(CommitTimeOrder::OldestFirst), - ] - .into_iter() - } + use super::*; + use crate::util::fixture; #[test] fn hidden_tip_with_longer_path_to_shared_ancestor() -> crate::Result { @@ -316,30 +201,31 @@ mod hide_with_graph_painting { // H(hidden) --> X --> Y --> shared // // Expected: only A is returned (shared is reachable from H) - let dir = gix_testtools::scripted_fixture_read_only_standalone("make_repo_for_hidden_bug.sh")?; + let dir = fixture("make_repo_for_hidden_bug.sh")?; let repo_path = dir.join("long_hidden_path"); - let store = gix_odb::at(repo_path.join(".git").join("objects"))?; + insta::assert_snapshot!(git_graph(&repo_path)?, @" + * b6cf469d740a02645b7b9f7cdb98977a6cd7e5ab (HEAD -> main) A + | * 2955979fbddb1bddb9e1b1ca993789cacf612b18 (hidden_branch) H + | * ae431c4e51a81a1df4ac22a52c4e247734ee3c9d X + | * ab31ef4cacc50169f2b1d753c1e4efd55d570bbc Y + |/ + * f1543941113388f8a194164420fd7da96f73c2ce shared + "); + let odb = gix_odb::at(repo_path.join(".git").join("objects"))?; - let commits = parse_commits(&repo_path)?; + let commits = parse_commit_names(&repo_path)?; let tip_a = commits["A"]; let hidden_h = commits["H"]; let shared = commits["shared"]; - // Expected: only A (shared is reachable from H) let expected = vec![tip_a]; for sorting in all_sortings() { - let oids: Vec<_> = Simple::new(Some(tip_a), &store) - .sorting(sorting)? - .parents(Parents::All) - .hide(Some(hidden_h))? - .map(|res| res.map(|info| info.id)) - .collect::, _>>()?; - + let result = traverse([tip_a], &odb, sorting, Parents::All, [hidden_h])?; assert_eq!( - oids, expected, + result, expected, "sorting = {sorting:?}: 'shared' ({shared}) should NOT be returned because it's \ - reachable from hidden tip H. Got: {oids:?}" + reachable from hidden tip H" ); } @@ -365,31 +251,31 @@ mod hide_with_graph_painting { // H(hidden) --------->+ // // Expected: A, B, C are returned (D is reachable from H) - let dir = gix_testtools::scripted_fixture_read_only_standalone("make_repo_for_hidden_bug.sh")?; + let dir = fixture("make_repo_for_hidden_bug.sh")?; let repo_path = dir.join("long_interesting_path"); - let store = gix_odb::at(repo_path.join(".git").join("objects"))?; + insta::assert_snapshot!(git_graph(&repo_path)?, @" + * 8822f888affa916a2c945ef3b17447f29f8aabff (HEAD -> main) A + * 90f80e3c031e9149cfa631493663ffe52d645aab B + * 2f353d445c4c552eec8e84f0f6f73999d08a8073 C + | * 7e0cf8f62783a0eb1043fbe56d220308c3e0289e (hidden_branch) H + |/ + * 359b53df58a6e26b95e276a9d1c9e2b33a3b50bf D + "); + let odb = gix_odb::at(repo_path.join(".git").join("objects"))?; - let commits = parse_commits(&repo_path)?; + let commits = parse_commit_names(&repo_path)?; let tip_a = commits["A"]; let hidden_h = commits["H"]; let d = commits["D"]; - // Expected: A, B, C (D is reachable from H) - let expected_commits = vec!["A", "B", "C"]; - let expected: Vec<_> = expected_commits.iter().map(|name| commits[*name]).collect(); + let expected: Vec<_> = ["A", "B", "C"].iter().map(|name| commits[*name]).collect(); for sorting in all_sortings() { - let oids: Vec<_> = Simple::new(Some(tip_a), &store) - .sorting(sorting)? - .parents(Parents::All) - .hide(Some(hidden_h))? - .map(|res| res.map(|info| info.id)) - .collect::, _>>()?; - + let result = traverse([tip_a], &odb, sorting, Parents::All, [hidden_h])?; assert_eq!( - oids, expected, + result, expected, "sorting = {sorting:?}: 'D' ({d}) should NOT be returned because it's \ - reachable from hidden tip H. Got: {oids:?}" + reachable from hidden tip H" ); } @@ -409,259 +295,293 @@ mod hide_with_graph_painting { } mod different_date_intermixed { - use gix_traverse::commit::simple::{CommitTimeOrder, Sorting}; - - use crate::commit::simple::TraversalAssertion; + use super::*; + use gix_traverse::commit::simple::CommitTimeOrder; #[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() + let odb = named_fixture_odb("make_repos.sh", "intermixed")?; + 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 { - 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() + let odb = named_fixture_odb("make_repos.sh", "intermixed")?; + 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(()) } } mod different_date { - use gix_traverse::commit::simple::{CommitTimeOrder, Sorting}; - - use crate::commit::simple::TraversalAssertion; + use super::*; + use gix_traverse::commit::simple::CommitTimeOrder; #[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() + let odb = named_fixture_odb("make_repos.sh", "simple")?; + 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 { - 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() + let odb = named_fixture_odb("make_repos.sh", "simple")?; + 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(()) } } /// 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 super::*; + use crate::util::fixture_odb; + use gix_hash::oid; + use gix_traverse::commit::simple::CommitTimeOrder; - use crate::{commit::simple::TraversalAssertion, hex_to_id}; + fn odb() -> crate::Result { + fixture_odb("make_traversal_repo_for_commits_same_date.sh") + } #[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() + let odb = odb()?; + 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 { - 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() + let odb = odb()?; + 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 { - 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() + let odb = odb()?; + 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 { - 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() + let odb = odb()?; + 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 { - 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() + let odb = odb()?; + 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] @@ -670,88 +590,107 @@ mod same_date { // 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")) + let odb = odb()?; + 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 odb = odb()?; + 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; - 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); + 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(()) } } /// 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 super::*; + use crate::util::{fixture, fixture_odb, git_graph}; + use gix_traverse::commit::simple::CommitTimeOrder; - use crate::commit::simple::git_graph; - use crate::{commit::simple::TraversalAssertion, hex_to_id}; + fn odb() -> crate::Result { + fixture_odb("make_traversal_repo_for_commits_with_dates.sh") + } #[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() + let odb = odb()?; + let tip = hex_to_id("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. + 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 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" + let dir = fixture("make_traversal_repo_for_commits_with_dates.sh")?; + let odb = gix_odb::at(dir.join(".git").join("objects"))?; + let tip = hex_to_id("288e509293165cb5630d08f4185bdf2445bf6170"); // m1b1 + + insta::assert_snapshot!(git_graph(&dir)?, @r" * 288e509293165cb5630d08f4185bdf2445bf6170 (HEAD -> main) m1b1 |\ | * bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac (branch1) b1c1 @@ -759,91 +698,131 @@ mod adjusted_dates { |/ * 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() + + // 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 { - 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()?; + let odb = odb()?; + 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 odb = odb()?; + let tip = hex_to_id("288e509293165cb5630d08f4185bdf2445bf6170"); // m1b1 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()?; + + // 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 { - 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 - })?; + let odb = odb()?; + 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!( - iter.count(), - 0, + count, 0, "initial tips that don't pass cutoff value are not returned either" ); } @@ -852,42 +831,19 @@ mod adjusted_dates { #[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()?; + let odb = odb()?; + 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(()) } - - 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/topo.rs b/gix-traverse/tests/traverse/commit/topo.rs index 0b9ffe6cfb..2ea6d92a3f 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(|id| id.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(|id| id.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(|id| id.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 81ecb3be5b..fd5fd2e474 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 003ecbf3b3..93ba1e8eae 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 0000000000..4bc6958b3a --- /dev/null +++ b/gix-traverse/tests/traverse/util.rs @@ -0,0 +1,77 @@ +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)) +} + +/// Get an object database handle for a named sub-repository within a fixture. +pub fn named_fixture_odb(script_name: &str, repo_name: &str) -> Result { + let (_path, odb) = named_fixture(script_name, repo_name)?; + Ok(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 { + use gix_object::bstr::{ByteSlice, ByteVec}; + 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 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) +} From ce539982e32379fec35e2bdaf4f7967617bdd244 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 3 Feb 2026 21:30:43 +0100 Subject: [PATCH 3/4] Deduplicate gix-traverse tests further --- gix-traverse/tests/traverse/commit/simple.rs | 207 ++++++++++++------- gix-traverse/tests/traverse/commit/topo.rs | 6 +- gix-traverse/tests/traverse/util.rs | 44 ++-- 3 files changed, 165 insertions(+), 92 deletions(-) diff --git a/gix-traverse/tests/traverse/commit/simple.rs b/gix-traverse/tests/traverse/commit/simple.rs index 09869d5b56..2ca159c916 100644 --- a/gix-traverse/tests/traverse/commit/simple.rs +++ b/gix-traverse/tests/traverse/commit/simple.rs @@ -1,5 +1,5 @@ use crate::hex_to_id; -use crate::util::{commit_graph, git_graph, named_fixture, named_fixture_odb, parse_commit_names}; +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}; @@ -73,7 +73,7 @@ mod hide { 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)?, @r" + insta::assert_snapshot!(git_graph(&repo_dir)?, @" * e07cf1277ff7c43090f1acfc85a46039e7de1272 (HEAD -> disjoint) b3 * 94cf3f3a4c782b672173423e7a4157a02957dd48 b2 * 34e5ff5ce3d3ba9f0a00d11a7fad72551fff0861 b1 @@ -91,7 +91,7 @@ mod hide { ]; for sorting in all_sortings() { - let result = traverse_both([tip], &odb, sorting, Parents::All, hidden.clone())?; + let result = traverse_both([tip], &odb, sorting, Parents::All, hidden)?; assert_eq!(result, expected, "sorting = {sorting:?}"); } Ok(()) @@ -99,26 +99,16 @@ mod hide { #[test] fn all_hidden() -> crate::Result { - let (repo_dir, odb) = named_fixture("make_repos.sh", "disjoint_branches")?; - - insta::assert_snapshot!(git_graph(&repo_dir)?, @r" - * e07cf1277ff7c43090f1acfc85a46039e7de1272 (HEAD -> disjoint) b3 - * 94cf3f3a4c782b672173423e7a4157a02957dd48 b2 - * 34e5ff5ce3d3ba9f0a00d11a7fad72551fff0861 b1 - * b5665181bf4c338ab16b10da0524d81b96aff209 (main) a3 - * f0230ce37b83d8e9f51ea6322ed7e8bd148d8e28 a2 - * 674aca0765b935ac5e7f7e9ab83af7f79272b5b0 a1 - "); - + 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.clone(); + let hidden = tips; for sorting in all_sortings() { - let result = traverse_both(tips.clone(), &odb, sorting, Parents::All, hidden.clone())?; + let result = traverse_both(tips, &odb, sorting, Parents::All, hidden)?; assert!(result.is_empty(), "sorting = {sorting:?}"); } Ok(()) @@ -168,7 +158,7 @@ mod hide { ]; for sorting in all_sortings() { - let result = traverse_both([tip_merge], &odb, sorting, Parents::All, hidden_branches.clone())?; + let result = traverse_both([tip_merge], &odb, sorting, Parents::All, hidden_branches)?; assert_eq!(result, expected, "sorting = {sorting:?}"); } @@ -184,14 +174,21 @@ mod hide { } mod hide_with_graph_painting { - //! These tests verify th + //! These tests verify the behavior of graph painting with hidden commits, particularly //! the relative path lengths between interesting and hidden tips to shared ancestors. //! //! The implementation must ensure all commits reachable from hidden tips are properly //! excluded, regardless of traversal order. use super::*; - use crate::util::fixture; + use crate::util::{fixture, git_rev_list}; + + 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 { @@ -201,8 +198,8 @@ mod hide_with_graph_painting { // H(hidden) --> X --> Y --> shared // // Expected: only A is returned (shared is reachable from H) - let dir = fixture("make_repo_for_hidden_bug.sh")?; - let repo_path = dir.join("long_hidden_path"); + 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 @@ -211,7 +208,6 @@ mod hide_with_graph_painting { |/ * f1543941113388f8a194164420fd7da96f73c2ce shared "); - let odb = gix_odb::at(repo_path.join(".git").join("objects"))?; let commits = parse_commit_names(&repo_path)?; let tip_a = commits["A"]; @@ -230,14 +226,7 @@ mod hide_with_graph_painting { } // Verify against git - let output = std::process::Command::new("git") - .current_dir(&repo_path) - .args(["rev-list", "main", "--not", "hidden_branch"]) - .output()?; - let git_output: Vec<_> = String::from_utf8_lossy(&output.stdout) - .lines() - .map(|s| hex_to_id(s.trim())) - .collect(); + 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(()) @@ -251,8 +240,8 @@ mod hide_with_graph_painting { // H(hidden) --------->+ // // Expected: A, B, C are returned (D is reachable from H) - let dir = fixture("make_repo_for_hidden_bug.sh")?; - let repo_path = dir.join("long_interesting_path"); + let (repo_path, odb) = hidden_bug_repo("long_interesting_path")?; + insta::assert_snapshot!(git_graph(&repo_path)?, @" * 8822f888affa916a2c945ef3b17447f29f8aabff (HEAD -> main) A * 90f80e3c031e9149cfa631493663ffe52d645aab B @@ -261,7 +250,6 @@ mod hide_with_graph_painting { |/ * 359b53df58a6e26b95e276a9d1c9e2b33a3b50bf D "); - let odb = gix_odb::at(repo_path.join(".git").join("objects"))?; let commits = parse_commit_names(&repo_path)?; let tip_a = commits["A"]; @@ -280,14 +268,7 @@ mod hide_with_graph_painting { } // Verify against git - let output = std::process::Command::new("git") - .current_dir(&repo_path) - .args(["rev-list", "main", "--not", "hidden_branch"]) - .output()?; - let git_output: Vec<_> = String::from_utf8_lossy(&output.stdout) - .lines() - .map(|s| hex_to_id(s.trim())) - .collect(); + 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(()) @@ -298,9 +279,30 @@ mod different_date_intermixed { 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 odb = named_fixture_odb("make_repos.sh", "intermixed")?; + 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, @@ -323,7 +325,8 @@ mod different_date_intermixed { #[test] fn head_date_order() -> crate::Result { - let odb = named_fixture_odb("make_repos.sh", "intermixed")?; + 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 @@ -374,9 +377,31 @@ mod different_date { 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 odb = named_fixture_odb("make_repos.sh", "simple")?; + 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, @@ -401,7 +426,8 @@ mod different_date { #[test] fn head_date_order() -> crate::Result { - let odb = named_fixture_odb("make_repos.sh", "simple")?; + 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 @@ -455,17 +481,33 @@ mod different_date { /// Same dates are somewhat special as they show how sorting-details on priority queues affects ordering mod same_date { use super::*; - use crate::util::fixture_odb; + use crate::util::fixture; use gix_hash::oid; use gix_traverse::commit::simple::CommitTimeOrder; - fn odb() -> crate::Result { - fixture_odb("make_traversal_repo_for_commits_same_date.sh") + 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 odb = odb()?; + 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 = [ @@ -482,7 +524,8 @@ mod same_date { #[test] fn head_breadth_first() -> crate::Result { - let odb = odb()?; + 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. @@ -506,7 +549,8 @@ mod same_date { #[test] fn head_date_order() -> crate::Result { - let odb = odb()?; + let (_repo_dir, odb) = same_date_repo()?; + // Graph shown in `c4_breadth_first` let tip = hex_to_id("01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"); // m1b1 let expected = [ @@ -543,7 +587,8 @@ mod same_date { #[test] fn head_first_parent_only_breadth_first() -> crate::Result { - let odb = odb()?; + let (_repo_dir, odb) = same_date_repo()?; + // Graph shown in `c4_breadth_first` let tip = hex_to_id("01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"); // m1b1 let expected = [ @@ -562,7 +607,8 @@ mod same_date { #[test] fn head_c4_breadth_first() -> crate::Result { - let odb = odb()?; + 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 @@ -590,7 +636,8 @@ mod same_date { // 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 odb = odb()?; + 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 @@ -621,7 +668,8 @@ mod same_date { 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 odb = odb()?; + 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 @@ -658,19 +706,33 @@ mod same_date { /// Some dates adjusted to be a year apart, but still 'c1' and 'c2' with the same date. mod adjusted_dates { use super::*; - use crate::util::{fixture, fixture_odb, git_graph}; + use crate::util::fixture; use gix_traverse::commit::simple::CommitTimeOrder; - fn odb() -> crate::Result { - fixture_odb("make_traversal_repo_for_commits_with_dates.sh") + 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 odb = odb()?; + 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 - // Here `git` also shows `b1c1` first, making topo-order similar to date order for some reason, + // Git also shows `b1c1` first, making topo-order similar to date order, // even though c2 *is* the first parent. let expected = [ tip, @@ -686,19 +748,10 @@ mod adjusted_dates { #[test] fn head_date_order() -> crate::Result { - let dir = fixture("make_traversal_repo_for_commits_with_dates.sh")?; - let odb = gix_odb::at(dir.join(".git").join("objects"))?; + let (_repo_dir, odb) = adjusted_dates_repo()?; + // Graph with timestamps shown in `head_breadth_first` let tip = hex_to_id("288e509293165cb5630d08f4185bdf2445bf6170"); // m1b1 - insta::assert_snapshot!(git_graph(&dir)?, @r" - * 288e509293165cb5630d08f4185bdf2445bf6170 (HEAD -> main) m1b1 - |\ - | * bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac (branch1) b1c1 - * | 9902e3c3e8f0c569b4ab295ddf473e6de763e1e7 c2 - |/ - * 134385f6d781b7e97062102c6a483440bfda2a03 c1 - "); - // NewestFirst let expected_newest = [ tip, @@ -736,7 +789,8 @@ mod adjusted_dates { #[test] fn head_date_order_with_cutoff() -> crate::Result { - let odb = odb()?; + let (_repo_dir, odb) = adjusted_dates_repo()?; + // Graph shown in `head_breadth_first` let tip = hex_to_id("288e509293165cb5630d08f4185bdf2445bf6170"); // m1b1 let expected = [ @@ -762,7 +816,8 @@ mod adjusted_dates { #[test] fn head_date_order_with_cutoff_disabled() -> crate::Result { - let odb = odb()?; + 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 @@ -809,7 +864,8 @@ mod adjusted_dates { #[test] fn date_order_with_cutoff_is_applied_to_starting_position() -> crate::Result { - let odb = odb()?; + 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] { @@ -831,7 +887,8 @@ mod adjusted_dates { #[test] fn head_date_order_first_parent_only() -> crate::Result { - let odb = odb()?; + let (_repo_dir, odb) = adjusted_dates_repo()?; + // Graph shown in `head_breadth_first` let tip = hex_to_id("288e509293165cb5630d08f4185bdf2445bf6170"); // m1b1 let expected = [ diff --git a/gix-traverse/tests/traverse/commit/topo.rs b/gix-traverse/tests/traverse/commit/topo.rs index 2ea6d92a3f..7897b610c7 100644 --- a/gix-traverse/tests/traverse/commit/topo.rs +++ b/gix-traverse/tests/traverse/commit/topo.rs @@ -124,7 +124,7 @@ mod basic { // Verify against baseline let baseline = read_baseline(&fixture_dir()?, "all-commits")?; - let expected_strs: Vec<_> = expected.iter().map(|id| id.to_string()).collect(); + let expected_strs: Vec<_> = expected.iter().map(std::string::ToString::to_string).collect(); assert_eq!(expected_strs, baseline, "Baseline must match the expectation"); Ok(()) @@ -278,7 +278,7 @@ mod first_parent { // Verify against baseline let baseline = read_baseline(&fixture_dir()?, "first-parent")?; - let expected_strs: Vec<_> = expected.iter().map(|id| id.to_string()).collect(); + let expected_strs: Vec<_> = expected.iter().map(std::string::ToString::to_string).collect(); assert_eq!(expected_strs, baseline, "Baseline must match the expectation"); Ok(()) @@ -359,7 +359,7 @@ mod date_order { // Verify against baseline let baseline = read_baseline(&fixture_dir()?, "date-order")?; - let expected_strs: Vec<_> = expected.iter().map(|id| id.to_string()).collect(); + 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/util.rs b/gix-traverse/tests/traverse/util.rs index 4bc6958b3a..1cb911c4d4 100644 --- a/gix-traverse/tests/traverse/util.rs +++ b/gix-traverse/tests/traverse/util.rs @@ -27,12 +27,6 @@ pub fn named_fixture(script_name: &str, repo_name: &str) -> Result<(PathBuf, gix Ok((repo_dir, odb)) } -/// Get an object database handle for a named sub-repository within a fixture. -pub fn named_fixture_odb(script_name: &str, repo_name: &str) -> Result { - let (_path, odb) = named_fixture(script_name, repo_name)?; - Ok(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() @@ -41,17 +35,25 @@ pub fn commit_graph(store: &gix_odb::Store) -> Option { /// 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", - "--pretty=format:%H %d %s", - ]) + .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()); @@ -75,3 +77,17 @@ pub fn parse_commit_names(repo_path: &std::path::Path) -> Result 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()) +} From 6a5b4e6fc8419412ddf6f6383dd4aec14566793e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 3 Feb 2026 21:45:15 +0100 Subject: [PATCH 4/4] chore: update `bytes` to a fixed version --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 768f20c9f3..756f82b9aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -363,9 +363,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bytesize"