use super::*; use crate::util::{commit_graph, fixture, git_rev_list, odb_at}; use std::{cell::Cell, rc::Rc}; fn assert_simple_repo_graph(repo_dir: &std::path::Path) -> crate::Result { let graph = git_graph(repo_dir)?; insta::allow_duplicates! { insta::assert_snapshot!(graph, @r" *-. Oid(1) (HEAD -> main) merge |\ \ | | * Oid(2) (branch2) b2c2 | | * Oid(3) b2c1 | * | Oid(4) (branch1) b1c2 | * | Oid(5) b1c1 | |/ * / Oid(6) c5 |/ * Oid(7) c4 * Oid(8) c3 * Oid(9) c2 * Oid(10) c1 ") } Ok(()) } #[test] fn disjoint_hidden_and_interesting() -> crate::Result { let (repo_dir, odb) = named_fixture("make_repos.sh", "disjoint_branches")?; insta::assert_snapshot!(git_graph(&repo_dir)?, @r" * Oid(1) (HEAD -> disjoint) b3 * Oid(2) b2 * Oid(3) b1 * Oid(4) (main) a3 * Oid(5) a2 * Oid(6) a1 "); let tip = hex_to_id("e07cf1277ff7c43090f1acfc85a46039e7de1272"); // b3 let hidden = [hex_to_id("b5665181bf4c338ab16b10da0524d81b96aff209")]; // a3 let expected = [ tip, hex_to_id("94cf3f3a4c782b672173423e7a4157a02957dd48"), // b2 hex_to_id("34e5ff5ce3d3ba9f0a00d11a7fad72551fff0861"), // b1 ]; for sorting in all_sortings() { let result = traverse_both([tip], &odb, sorting, Parents::All, hidden)?; assert_eq!(result, expected, "sorting = {sorting:?}"); } Ok(()) } #[test] fn all_hidden() -> crate::Result { let (_repo_dir, odb) = named_fixture("make_repos.sh", "disjoint_branches")?; let tips = [ hex_to_id("e07cf1277ff7c43090f1acfc85a46039e7de1272"), // b3 hex_to_id("b5665181bf4c338ab16b10da0524d81b96aff209"), // a3 ]; // The start positions are also declared hidden, so nothing should be visible. let hidden = tips; for sorting in all_sortings() { let result = traverse_both(tips, &odb, sorting, Parents::All, hidden)?; assert!(result.is_empty(), "sorting = {sorting:?}"); } Ok(()) } #[test] fn some_hidden_and_all_hidden() -> crate::Result { let (repo_dir, odb) = named_fixture("make_repos.sh", "simple")?; assert_simple_repo_graph(&repo_dir)?; // Test: Hidden has to catch up with non-hidden let tip_c2 = hex_to_id("ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83"); let hidden_c5 = hex_to_id("0edb95c0c0d9933d88f532ec08fcd405d0eee882"); for sorting in all_sortings() { let result = traverse_both([tip_c2], &odb, sorting, Parents::All, [hidden_c5])?; assert!( result.is_empty(), "c2 is reachable from hidden c5, sorting = {sorting:?}" ); } // Test: merge tip with two branch tips hidden let tip_merge = hex_to_id("f49838d84281c3988eeadd988d97dd358c9f9dc4"); let hidden_branches = [ hex_to_id("48e8dac19508f4238f06c8de2b10301ce64a641c"), // b2c2 hex_to_id("66a309480201c4157b0eae86da69f2d606aadbe7"), // b1c2 ]; let expected = [ tip_merge, hex_to_id("0edb95c0c0d9933d88f532ec08fcd405d0eee882"), // c5 ]; for sorting in all_sortings() { let result = traverse_both([tip_merge], &odb, sorting, Parents::All, hidden_branches)?; assert_eq!(result, expected, "sorting = {sorting:?}"); } // Test: single-parent mode with hidden catching up let tip_b1c1 = hex_to_id("80947acb398362d8236fcb8bf0f8a9dac640583f"); let hidden_merge = hex_to_id("f49838d84281c3988eeadd988d97dd358c9f9dc4"); let result = traverse_both([tip_b1c1], &odb, Sorting::BreadthFirst, Parents::First, [hidden_merge])?; assert!(result.is_empty(), "b1c1 is reachable from hidden merge"); Ok(()) } fn hidden_bug_repo(name: &str) -> crate::Result<(std::path::PathBuf, gix_odb::Handle)> { let dir = fixture("make_repo_for_hidden_bug.sh")?; let repo_path = dir.join(name); let odb = odb_at(repo_path.join(".git").join("objects"))?; Ok((repo_path, odb)) } #[test] fn hidden_tip_with_longer_path_to_shared_ancestor() -> crate::Result { // Graph: // A(tip) --> shared // / // H(hidden) --> X --> Y --> shared // // Expected: only A is returned (shared is reachable from H) let (repo_path, odb) = hidden_bug_repo("long_hidden_path")?; insta::assert_snapshot!(git_graph(&repo_path)?, @r" * Oid(1) (HEAD -> main) A | * Oid(2) (hidden_branch) H | * Oid(3) X | * Oid(4) Y |/ * Oid(5) shared " ); let commits = parse_commit_names(&repo_path)?; let tip_a = commits["A"]; let hidden_h = commits["H"]; let shared = commits["shared"]; let expected = vec![tip_a]; for sorting in all_sortings() { let result = traverse([tip_a], &odb, sorting, Parents::All, [hidden_h])?; assert_eq!( result, expected, "sorting = {sorting:?}: 'shared' ({shared}) should NOT be returned because it's \ reachable from hidden tip H" ); } // Verify against git let git_output = git_rev_list(&repo_path, &["main", "--not", "hidden_branch"])?; assert_eq!(git_output, expected, "git rev-list should show only A"); Ok(()) } #[test] fn interesting_tip_with_longer_path_to_shared_ancestor() -> crate::Result { // Graph: // A(tip) --> B --> C --> D(shared) // / // H(hidden) --------->+ // // Expected: A, B, C are returned (D is reachable from H) let (repo_path, odb) = hidden_bug_repo("long_interesting_path")?; insta::assert_snapshot!(git_graph(&repo_path)?, @r" * Oid(1) (HEAD -> main) A * Oid(2) B * Oid(3) C | * Oid(4) (hidden_branch) H |/ * Oid(5) D "); let commits = parse_commit_names(&repo_path)?; let tip_a = commits["A"]; let hidden_h = commits["H"]; let d = commits["D"]; let expected: Vec<_> = ["A", "B", "C"].iter().map(|name| commits[*name]).collect(); for sorting in all_sortings() { let result = traverse([tip_a], &odb, sorting, Parents::All, [hidden_h])?; assert_eq!( result, expected, "sorting = {sorting:?}: 'D' ({d}) should NOT be returned because it's \ reachable from hidden tip H" ); } // Verify against git let git_output = git_rev_list(&repo_path, &["main", "--not", "hidden_branch"])?; assert_eq!(git_output, expected, "git rev-list should show A, B, C"); Ok(()) } #[test] fn without_commit_graph_still_hides_single_visible_tip_correctly() -> crate::Result { let (repo_dir, odb) = named_fixture("make_repos.sh", "simple")?; assert_simple_repo_graph(&repo_dir)?; let tip_c2 = hex_to_id("ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83"); let hidden_c5 = hex_to_id("0edb95c0c0d9933d88f532ec08fcd405d0eee882"); let result: Vec<_> = Simple::new([tip_c2], &odb) .sorting(Sorting::BreadthFirst)? .parents(Parents::All) .commit_graph(None) .hide([hidden_c5])? .map(|res| res.map(|info| info.id)) .collect::, _>>()?; assert!(result.is_empty(), "c2 is reachable from hidden c5"); Ok(()) } #[test] fn commit_graph_reduces_odb_lookups_when_hidden_tips_cover_visible_tips() -> crate::Result { let (repo_dir, odb) = named_fixture("make_repos.sh", "simple")?; assert_simple_repo_graph(&repo_dir)?; let tips = [ hex_to_id("ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83"), // c2 hex_to_id("33aa07785dd667c0196064e3be3c51dd9b4744ef"), // c3 ]; let hidden_c5 = hex_to_id("0edb95c0c0d9933d88f532ec08fcd405d0eee882"); let without_graph_lookups = Rc::new(Cell::new(0usize)); let with_graph_lookups = Rc::new(Cell::new(0usize)); let without_graph: Vec<_> = Simple::new( tips, CountingFind { inner: &odb, lookups: Rc::clone(&without_graph_lookups), }, ) .sorting(Sorting::BreadthFirst)? .parents(Parents::All) .commit_graph(None) .hide([hidden_c5])? .map(|res| res.map(|info| info.id)) .collect::, _>>()?; let with_graph: Vec<_> = Simple::new( tips, CountingFind { inner: &odb, lookups: Rc::clone(&with_graph_lookups), }, ) .sorting(Sorting::BreadthFirst)? .parents(Parents::All) .commit_graph(commit_graph(odb.store_ref())) .hide([hidden_c5])? .map(|res| res.map(|info| info.id)) .collect::, _>>()?; assert!( without_graph.is_empty(), "both starting tips are reachable from hidden c5" ); assert_eq!( without_graph, with_graph, "commit-graph must not change traversal results" ); assert!( with_graph_lookups.get() < without_graph_lookups.get(), "commit-graph should reduce object database lookups for hidden painting", ); Ok(()) } #[test] fn hide_and_commit_graph_call_order_do_not_matter() -> crate::Result { let (repo_dir, odb) = named_fixture("make_repos.sh", "simple")?; assert_simple_repo_graph(&repo_dir)?; let tip_merge = hex_to_id("f49838d84281c3988eeadd988d97dd358c9f9dc4"); let hidden_branches = [ hex_to_id("48e8dac19508f4238f06c8de2b10301ce64a641c"), hex_to_id("66a309480201c4157b0eae86da69f2d606aadbe7"), ]; let hide_then_graph: Vec<_> = Simple::new([tip_merge], &odb) .sorting(Sorting::BreadthFirst)? .parents(Parents::All) .hide(hidden_branches)? .commit_graph(commit_graph(odb.store_ref())) .map(|res| res.map(|info| info.id)) .collect::, _>>()?; let graph_then_hide: Vec<_> = Simple::new([tip_merge], &odb) .sorting(Sorting::BreadthFirst)? .parents(Parents::All) .commit_graph(commit_graph(odb.store_ref())) .hide(hidden_branches)? .map(|res| res.map(|info| info.id)) .collect::, _>>()?; assert_eq!(hide_then_graph, graph_then_hide); Ok(()) } struct CountingFind<'a> { inner: &'a gix_odb::Handle, lookups: Rc>, } impl gix_object::Find for CountingFind<'_> { fn try_find<'a>( &self, id: &gix_hash::oid, buffer: &'a mut Vec, ) -> Result>, gix_object::find::Error> { self.lookups.set(self.lookups.get() + 1); gix_object::Find::try_find(self.inner, id, buffer) } }