use gix_diff::{ Rewrites, rewrites::{Copies, CopySource}, tree::{recorder::Location, visit::Relation}, tree_with_rewrites::{Change, Options}, }; use gix_object::{TreeRefIter, bstr::BStr}; #[test] fn empty_to_new_tree_without_rename_tracking() -> crate::Result { let (changes, _out) = collect_changes(None, "c1 - initial").expect("full path tracking is the default"); insta::assert_snapshot!(crate::normalize_debug_snapshot(&changes).0, @r#" [ Addition { location: "a", relation: None, entry_mode: EntryMode(0o100644), id: Oid(1), }, Addition { location: "b", relation: None, entry_mode: EntryMode(0o100644), id: Oid(1), }, Addition { location: "d", relation: None, entry_mode: EntryMode(0o100644), id: Oid(1), }, Addition { location: "dir", relation: Some( Parent( 1, ), ), entry_mode: EntryMode(0o40000), id: Oid(2), }, Addition { location: "dir/c", relation: Some( ChildOfParent( 1, ), ), entry_mode: EntryMode(0o100644), id: Oid(1), }, ] "#); let (changes, _out) = collect_changes_opts( None, "c1 - initial", Options { location: Some(Location::FileName), ..Default::default() }, ) .expect("the path-options are respected - we only see the filename here"); insta::assert_snapshot!(crate::normalize_debug_snapshot(&changes).0, @r#" [ Addition { location: "a", relation: None, entry_mode: EntryMode(0o100644), id: Oid(1), }, Addition { location: "b", relation: None, entry_mode: EntryMode(0o100644), id: Oid(1), }, Addition { location: "d", relation: None, entry_mode: EntryMode(0o100644), id: Oid(1), }, Addition { location: "dir", relation: Some( Parent( 1, ), ), entry_mode: EntryMode(0o40000), id: Oid(2), }, Addition { location: "c", relation: Some( ChildOfParent( 1, ), ), entry_mode: EntryMode(0o100644), id: Oid(1), }, ] "#); { let (lhs, rhs, mut cache, odb) = repo_with_trees(None, "c1 - initial")?; let err = gix_diff::tree_with_rewrites( TreeRefIter::from_bytes(&lhs, gix_testtools::object_hash()), TreeRefIter::from_bytes(&rhs, gix_testtools::object_hash()), &mut cache, &mut Default::default(), &odb, |_change| Err(std::io::Error::other("custom error")), Options::default(), ) .unwrap_err(); assert_eq!( err.to_string(), "The user-provided callback failed", "custom errors made visible and not squelched" ); } Ok(()) } #[test] fn changes_against_modified_tree_with_filename_tracking() -> crate::Result { let (changes, _out) = collect_changes("c2", "c3-modification")?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&changes).0, @r#" [ Modification { location: "a", previous_entry_mode: EntryMode(0o100644), previous_id: Oid(1), entry_mode: EntryMode(0o100644), id: Oid(2), }, Modification { location: "dir", previous_entry_mode: EntryMode(0o40000), previous_id: Oid(3), entry_mode: EntryMode(0o40000), id: Oid(4), }, Modification { location: "dir/c", previous_entry_mode: EntryMode(0o100644), previous_id: Oid(5), entry_mode: EntryMode(0o100644), id: Oid(6), }, ] "#); let (changes, _out) = collect_changes_opts( "c2", "c3-modification", Options { location: Some(Location::FileName), ..Default::default() }, )?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&changes).0, @r#" [ Modification { location: "a", previous_entry_mode: EntryMode(0o100644), previous_id: Oid(1), entry_mode: EntryMode(0o100644), id: Oid(2), }, Modification { location: "dir", previous_entry_mode: EntryMode(0o40000), previous_id: Oid(3), entry_mode: EntryMode(0o40000), id: Oid(4), }, Modification { location: "c", previous_entry_mode: EntryMode(0o100644), previous_id: Oid(5), entry_mode: EntryMode(0o100644), id: Oid(6), }, ] "#); Ok(()) } #[test] fn renames_by_identity() -> crate::Result { for (from, to, expected, assert_msg, track_empty) in [ ( "c3-modification", "r1-identity", vec![BStr::new("a"), "dir/a-moved".into()], "one rename and nothing else", false, ), ( "c4 - add identical files", "r2-ambiguous", vec![ "s1".into(), "b1".into(), "s2".into(), "b2".into(), "s3".into(), "z".into(), ], "multiple possible sources decide by ordering everything lexicographically", true, ), ( "c4 - add identical files", "r2-ambiguous", vec![], "nothing is tracked with `track_empty = false`", false, ), ( "c5 - add links", "r4-symlinks", vec!["link-1".into(), "renamed-link-1".into()], "symlinks are only tracked by identity", false, ), ( "r1-identity", "c4 - add identical files", vec![], "not having any renames is OK as well", false, ), ( "tc1-identity", "tc1-identity", vec![], "copy tracking is off by default", false, ), ] { for percentage in [None, Some(0.5)] { let (changes, out) = collect_changes_opts( from, to, Options { location: Some(Location::Path), rewrites: Some(Rewrites { percentage, track_empty, ..Default::default() }), }, )?; let actual: Vec<_> = changes .into_iter() .filter(|c| !c.entry_mode().is_tree()) .flat_map(|c| match c { Change::Rewrite { source_location, location, copy, .. } => { assert!(!copy); vec![source_location, location] } _ => vec![], }) .collect(); assert_eq!(actual, expected, "{assert_msg}"); #[cfg(not(windows))] assert_eq!( out.expect("present as rewrites are configured").num_similarity_checks, 0, "there are no fuzzy checks in if everything was resolved by identity only" ); } } Ok(()) } #[test] fn rename_by_similarity() -> crate::Result { insta::allow_duplicates! { for percentage in [ None, Some(0.76), /*cutoff point where git stops seeing it as equal */ ] { let (changes, out) = collect_changes_opts( "r2-ambiguous", "r3-simple", Options { location: Some(Location::Path), rewrites: Some(Rewrites { percentage, ..Default::default() }), }, ).expect("errors can only happen with IO or ODB access fails"); insta::assert_snapshot!(crate::normalize_debug_snapshot(&( changes)).0, @r#" [ Modification { location: "b", previous_entry_mode: EntryMode(0o100644), previous_id: Oid(1), entry_mode: EntryMode(0o100644), id: Oid(2), }, Modification { location: "dir", previous_entry_mode: EntryMode(0o40000), previous_id: Oid(3), entry_mode: EntryMode(0o40000), id: Oid(4), }, Deletion { location: "dir/c", relation: None, entry_mode: EntryMode(0o100644), id: Oid(5), }, Addition { location: "dir/c-moved", relation: None, entry_mode: EntryMode(0o100644), id: Oid(6), }, ] "# ); let out = out.expect("tracking enabled"); assert_eq!(out.num_similarity_checks, if percentage.is_some() { 1 } else { 0 }); assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); } } let (changes, out) = collect_changes_opts( "r2-ambiguous", "r3-simple", Options { location: Some(Location::Path), rewrites: Some(Rewrites { percentage: Some(0.6), limit: 1, // has no effect as it's just one item here. ..Default::default() }), }, ) .expect("it found all items at the cut-off point, similar to git"); insta::assert_snapshot!(crate::normalize_debug_snapshot(&changes).0, @r#" [ Modification { location: "b", previous_entry_mode: EntryMode(0o100644), previous_id: Oid(1), entry_mode: EntryMode(0o100644), id: Oid(2), }, Modification { location: "dir", previous_entry_mode: EntryMode(0o40000), previous_id: Oid(3), entry_mode: EntryMode(0o40000), id: Oid(4), }, Rewrite { source_location: "dir/c", source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Oid(5), diff: Some( DiffLineStats { removals: 0, insertions: 1, before: 2, after: 3, similarity: 0.65, }, ), entry_mode: EntryMode(0o100644), id: Oid(6), location: "dir/c-moved", relation: None, copy: false, }, ] "#); let out = out.expect("tracking enabled"); assert_eq!(out.num_similarity_checks, 1); assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); Ok(()) } #[test] fn renames_by_similarity_with_limit() -> crate::Result { let (changes, out) = collect_changes_opts( "c6", "r5", Options { location: Some(Location::Path), rewrites: Some(Rewrites { limit: 1, // prevent fuzzy tracking from happening ..Default::default() }), }, )?; assert_eq!( changes.iter().filter(|c| matches!(c, Change::Rewrite { .. })).count(), 0, "fuzzy tracking is effectively disabled due to limit" ); let actual: Vec<_> = changes.iter().map(Change::location).collect(); assert_eq!(actual, ["f1", "f1-renamed", "f2", "f2-renamed"]); let out = out.expect("tracking enabled"); assert_eq!(out.num_similarity_checks, 0); assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 4); assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); Ok(()) } #[test] fn copies_by_identity() -> crate::Result { let (changes, out) = collect_changes_opts( "c7", "tc1-identity", Options { location: Some(Location::Path), rewrites: Some(Rewrites { copies: Some(Copies { source: CopySource::FromSetOfModifiedFiles, percentage: None, }), limit: 1, // the limit isn't actually used for identity based checks ..Default::default() }), }, )?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&changes).0, @r#" [ Modification { location: "dir", previous_entry_mode: EntryMode(0o40000), previous_id: Oid(1), entry_mode: EntryMode(0o40000), id: Oid(2), }, Rewrite { source_location: "base", source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Oid(3), diff: None, entry_mode: EntryMode(0o100644), id: Oid(3), location: "c1", relation: None, copy: true, }, Rewrite { source_location: "base", source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Oid(3), diff: None, entry_mode: EntryMode(0o100644), id: Oid(3), location: "c2", relation: None, copy: true, }, Rewrite { source_location: "base", source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Oid(3), diff: None, entry_mode: EntryMode(0o100644), id: Oid(3), location: "dir/c3", relation: None, copy: true, }, ] "#); let out = out.expect("tracking enabled"); assert_eq!(out.num_similarity_checks, 0); assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); Ok(()) } #[test] fn copies_by_similarity() -> crate::Result { let (changes, out) = collect_changes_opts( "tc1-identity", "tc2-similarity", Options { location: Some(Location::Path), rewrites: Some(Rewrites { copies: Some(Copies::default()), ..Default::default() }), }, )?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&changes).0, @r#" [ Modification { location: "dir", previous_entry_mode: EntryMode(0o40000), previous_id: Oid(1), entry_mode: EntryMode(0o40000), id: Oid(2), }, Rewrite { source_location: "base", source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Oid(3), diff: None, entry_mode: EntryMode(0o100644), id: Oid(3), location: "c4", relation: None, copy: true, }, Rewrite { source_location: "base", source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Oid(3), diff: Some( DiffLineStats { removals: 0, insertions: 1, before: 11, after: 12, similarity: 0.8888889, }, ), entry_mode: EntryMode(0o100644), id: Oid(4), location: "c5", relation: None, copy: true, }, Rewrite { source_location: "base", source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Oid(3), diff: Some( DiffLineStats { removals: 0, insertions: 1, before: 11, after: 12, similarity: 0.8888889, }, ), entry_mode: EntryMode(0o100644), id: Oid(5), location: "dir/c6", relation: None, copy: true, }, ] "#); let out = out.expect("tracking enabled"); assert_eq!( out.num_similarity_checks, 2, "two are similar, the other one is identical" ); assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); Ok(()) } #[test] fn copies_in_entire_tree_by_similarity() -> crate::Result { let (changes, out) = collect_changes_opts( "tc2-similarity", "tc3-find-harder", Options { location: Some(Location::Path), rewrites: Some(Rewrites { copies: Some(Copies::default()), ..Default::default() }), }, )?; assert_eq!( changes.iter().filter(|c| matches!(c, Change::Rewrite { .. })).count(), 0, "needs --find-copies-harder to detect rewrites here" ); let actual: Vec<_> = changes.iter().map(Change::location).collect(); assert_eq!(actual, ["b", "c6", "c7", "newly-added"]); let out = out.expect("tracking enabled"); assert_eq!( out.num_similarity_checks, 3, "it does have some candidates, probably for rename tracking" ); assert_eq!( out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0, "no limit configured" ); assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); let (changes, out) = collect_changes_opts( "tc2-similarity", "tc3-find-harder", Options { location: Some(Location::Path), rewrites: Some(Rewrites { copies: Some(Copies { source: CopySource::FromSetOfModifiedFilesAndAllSources, ..Default::default() }), ..Default::default() }), }, )?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&changes).0, @r#" [ Rewrite { source_location: "base", source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Oid(1), diff: None, entry_mode: EntryMode(0o100644), id: Oid(1), location: "c6", relation: None, copy: true, }, Rewrite { source_location: "dir/c6", source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Oid(2), diff: None, entry_mode: EntryMode(0o100644), id: Oid(2), location: "c7", relation: None, copy: true, }, Rewrite { source_location: "c5", source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Oid(3), diff: Some( DiffLineStats { removals: 0, insertions: 3, before: 12, after: 15, similarity: 0.75, }, ), entry_mode: EntryMode(0o100644), id: Oid(4), location: "newly-added", relation: None, copy: true, }, Modification { location: "b", previous_entry_mode: EntryMode(0o100644), previous_id: Oid(5), entry_mode: EntryMode(0o100644), id: Oid(6), }, ] "#); let out = out.expect("tracking enabled"); assert_eq!(out.num_similarity_checks, 22); assert_eq!( out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0, "no limit configured" ); assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); Ok(()) } #[test] fn copies_in_entire_tree_by_similarity_with_limit() -> crate::Result { let (changes, out) = collect_changes_opts( "tc2-similarity", "tc3-find-harder", Options { location: Some(Location::Path), rewrites: Some(Rewrites { copies: Some(Copies { source: CopySource::FromSetOfModifiedFilesAndAllSources, ..Default::default() }), limit: 2, // similarity checks can't be made that way track_empty: false, ..Default::default() }), }, )?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&changes).0, @r#" [ Rewrite { source_location: "base", source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Oid(1), diff: None, entry_mode: EntryMode(0o100644), id: Oid(1), location: "c6", relation: None, copy: true, }, Rewrite { source_location: "dir/c6", source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Oid(2), diff: None, entry_mode: EntryMode(0o100644), id: Oid(2), location: "c7", relation: None, copy: true, }, Modification { location: "b", previous_entry_mode: EntryMode(0o100644), previous_id: Oid(3), entry_mode: EntryMode(0o100644), id: Oid(4), }, Addition { location: "newly-added", relation: None, entry_mode: EntryMode(0o100644), id: Oid(5), }, ] "#); let out = out.expect("tracking enabled"); assert_eq!(out.num_similarity_checks, 0, "similarity checks can't run"); assert_eq!( out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0, "no limit configured" ); assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 19); Ok(()) } #[test] fn copies_by_similarity_with_limit() -> crate::Result { let (changes, out) = collect_changes_opts( "tc1-identity", "tc2-similarity", Options { location: Some(Location::Path), rewrites: Some(Rewrites { copies: Some(Copies::default()), limit: 1, ..Default::default() }), }, )?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&changes).0, @r#" [ Modification { location: "dir", previous_entry_mode: EntryMode(0o40000), previous_id: Oid(1), entry_mode: EntryMode(0o40000), id: Oid(2), }, Rewrite { source_location: "base", source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Oid(3), diff: None, entry_mode: EntryMode(0o100644), id: Oid(3), location: "c4", relation: None, copy: true, }, Addition { location: "c5", relation: None, entry_mode: EntryMode(0o100644), id: Oid(4), }, Addition { location: "dir/c6", relation: None, entry_mode: EntryMode(0o100644), id: Oid(5), }, ] "#); let out = out.expect("tracking enabled"); assert_eq!(out.num_similarity_checks, 0); assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); assert_eq!( out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 2, "limit prevents any similarity check from being performed, and identity fails in most places" ); Ok(()) } #[test] fn realistic_renames_by_identity() -> crate::Result { let (changes, out) = collect_changes_opts( "r1-base", "r1-change", Options { location: Some(Location::Path), rewrites: Some(Rewrites { copies: Some(Copies::default()), limit: 1, track_empty: true, ..Default::default() }), }, )?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&(changes.into_iter().filter(|c| !c.entry_mode().is_tree()).collect::>())).0, @r#" [ Rewrite { source_location: "git-index/src/file.rs", source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Oid(1), diff: None, entry_mode: EntryMode(0o100644), id: Oid(1), location: "git-index/src/file/mod.rs", relation: None, copy: false, }, Addition { location: "git-index/tests/index/file/access.rs", relation: None, entry_mode: EntryMode(0o100644), id: Oid(1), }, Modification { location: "git-index/tests/index/file/mod.rs", previous_entry_mode: EntryMode(0o100644), previous_id: Oid(1), entry_mode: EntryMode(0o100644), id: Oid(2), }, ] "#); #[cfg(not(windows))] { let actual = std::fs::read_to_string(repo_workdir()?.join("baseline.with-renames"))?; let expected = r#"commit 0231f5093bd3d760e7ee82984e0453da80e05c87 Author: author Date: Sat Jan 1 00:00:00 2000 +0000 r1-change diff --git a/git-index/src/file.rs b/git-index/src/file/mod.rs similarity index 100% rename from git-index/src/file.rs rename to git-index/src/file/mod.rs diff --git a/git-index/tests/index/file/access.rs b/git-index/tests/index/file/access.rs new file mode 100644 index 0000000..e69de29 diff --git a/git-index/tests/index/file/mod.rs b/git-index/tests/index/file/mod.rs index e69de29..8ba3a16 100644 --- a/git-index/tests/index/file/mod.rs +++ b/git-index/tests/index/file/mod.rs @@ -0,0 +1 @@ +n "#; crate::assert_hash_agnostic_patch_eq(&actual, expected); } let out = out.expect("tracking enabled"); assert_eq!(out.num_similarity_checks, 1); assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); Ok(()) } #[test] fn realistic_renames_disabled() -> crate::Result { let (changes, out) = collect_changes_opts( "r1-base", "r1-change", Options { location: Some(Location::Path), rewrites: None, }, )?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&(changes.into_iter().filter(|c| !c.entry_mode().is_tree()).collect::>())).0, @r#" [ Deletion { location: "git-index/src/file.rs", relation: None, entry_mode: EntryMode(0o100644), id: Oid(1), }, Addition { location: "git-index/src/file/mod.rs", relation: None, entry_mode: EntryMode(0o100644), id: Oid(1), }, Addition { location: "git-index/tests/index/file/access.rs", relation: None, entry_mode: EntryMode(0o100644), id: Oid(1), }, Modification { location: "git-index/tests/index/file/mod.rs", previous_entry_mode: EntryMode(0o100644), previous_id: Oid(1), entry_mode: EntryMode(0o100644), id: Oid(2), }, ] "#); #[cfg(not(windows))] { let actual = std::fs::read_to_string(repo_workdir()?.join("baseline.no-renames"))?; let expected = r#"commit 0231f5093bd3d760e7ee82984e0453da80e05c87 Author: author Date: Sat Jan 1 00:00:00 2000 +0000 r1-change diff --git a/git-index/src/file.rs b/git-index/src/file.rs deleted file mode 100644 index e69de29..0000000 diff --git a/git-index/src/file/mod.rs b/git-index/src/file/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/git-index/tests/index/file/access.rs b/git-index/tests/index/file/access.rs new file mode 100644 index 0000000..e69de29 diff --git a/git-index/tests/index/file/mod.rs b/git-index/tests/index/file/mod.rs index e69de29..8ba3a16 100644 --- a/git-index/tests/index/file/mod.rs +++ b/git-index/tests/index/file/mod.rs @@ -0,0 +1 @@ +n "#; crate::assert_hash_agnostic_patch_eq(&actual, expected); } assert_eq!(out, None, "tracking is disabled completely"); Ok(()) } #[test] fn realistic_renames_disabled_2() -> crate::Result { let (changes, out) = collect_changes_opts( "r2-base", "r2-change", Options { location: Some(Location::Path), rewrites: None, }, )?; // Directories are associated with their children, making a bundling possible. insta::assert_snapshot!(crate::normalize_debug_snapshot(&(changes.into_iter() .filter(|c| !c.entry_mode().is_tree() || c.relation().is_some_and(|r| matches!(r, Relation::Parent(_))) ).collect::>())).0, @r#" [ Deletion { location: "git-sec", relation: Some( Parent( 1, ), ), entry_mode: EntryMode(0o40000), id: Oid(1), }, Addition { location: "gix-sec", relation: Some( Parent( 2, ), ), entry_mode: EntryMode(0o40000), id: Oid(1), }, Deletion { location: "git-sec/CHANGELOG.md", relation: Some( ChildOfParent( 1, ), ), entry_mode: EntryMode(0o100644), id: Oid(2), }, Deletion { location: "git-sec/Cargo.toml", relation: Some( ChildOfParent( 1, ), ), entry_mode: EntryMode(0o100644), id: Oid(2), }, Addition { location: "gix-sec/CHANGELOG.md", relation: Some( ChildOfParent( 2, ), ), entry_mode: EntryMode(0o100644), id: Oid(2), }, Addition { location: "gix-sec/Cargo.toml", relation: Some( ChildOfParent( 2, ), ), entry_mode: EntryMode(0o100644), id: Oid(2), }, Deletion { location: "git-sec/src/identity.rs", relation: Some( ChildOfParent( 1, ), ), entry_mode: EntryMode(0o100644), id: Oid(2), }, Deletion { location: "git-sec/src/lib.rs", relation: Some( ChildOfParent( 1, ), ), entry_mode: EntryMode(0o100644), id: Oid(2), }, Deletion { location: "git-sec/src/permission.rs", relation: Some( ChildOfParent( 1, ), ), entry_mode: EntryMode(0o100644), id: Oid(2), }, Deletion { location: "git-sec/src/trust.rs", relation: Some( ChildOfParent( 1, ), ), entry_mode: EntryMode(0o100644), id: Oid(2), }, Deletion { location: "git-sec/tests/sec.rs", relation: Some( ChildOfParent( 1, ), ), entry_mode: EntryMode(0o100644), id: Oid(2), }, Addition { location: "gix-sec/src/identity.rs", relation: Some( ChildOfParent( 2, ), ), entry_mode: EntryMode(0o100644), id: Oid(2), }, Addition { location: "gix-sec/src/lib.rs", relation: Some( ChildOfParent( 2, ), ), entry_mode: EntryMode(0o100644), id: Oid(2), }, Addition { location: "gix-sec/src/permission.rs", relation: Some( ChildOfParent( 2, ), ), entry_mode: EntryMode(0o100644), id: Oid(2), }, Addition { location: "gix-sec/src/trust.rs", relation: Some( ChildOfParent( 2, ), ), entry_mode: EntryMode(0o100644), id: Oid(2), }, Addition { location: "gix-sec/tests/sec.rs", relation: Some( ChildOfParent( 2, ), ), entry_mode: EntryMode(0o100644), id: Oid(2), }, Deletion { location: "git-sec/tests/identity/mod.rs", relation: Some( ChildOfParent( 1, ), ), entry_mode: EntryMode(0o100644), id: Oid(2), }, Addition { location: "gix-sec/tests/identity/mod.rs", relation: Some( ChildOfParent( 2, ), ), entry_mode: EntryMode(0o100644), id: Oid(2), }, ] "#); #[cfg(not(windows))] { let expected = r#"commit d78c63c5ea3149040767e4387e7fc743cda118fd Author: author Date: Sat Jan 1 00:00:00 2000 +0000 r2-change diff --git a/git-sec/CHANGELOG.md b/git-sec/CHANGELOG.md deleted file mode 100644 index e69de29..0000000 diff --git a/git-sec/Cargo.toml b/git-sec/Cargo.toml deleted file mode 100644 index e69de29..0000000 diff --git a/git-sec/src/identity.rs b/git-sec/src/identity.rs deleted file mode 100644 index e69de29..0000000 diff --git a/git-sec/src/lib.rs b/git-sec/src/lib.rs deleted file mode 100644 index e69de29..0000000 diff --git a/git-sec/src/permission.rs b/git-sec/src/permission.rs deleted file mode 100644 index e69de29..0000000 diff --git a/git-sec/src/trust.rs b/git-sec/src/trust.rs deleted file mode 100644 index e69de29..0000000 diff --git a/git-sec/tests/identity/mod.rs b/git-sec/tests/identity/mod.rs deleted file mode 100644 index e69de29..0000000 diff --git a/git-sec/tests/sec.rs b/git-sec/tests/sec.rs deleted file mode 100644 index e69de29..0000000 diff --git a/gix-sec/CHANGELOG.md b/gix-sec/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/gix-sec/Cargo.toml b/gix-sec/Cargo.toml new file mode 100644 index 0000000..e69de29 diff --git a/gix-sec/src/identity.rs b/gix-sec/src/identity.rs new file mode 100644 index 0000000..e69de29 diff --git a/gix-sec/src/lib.rs b/gix-sec/src/lib.rs new file mode 100644 index 0000000..e69de29 diff --git a/gix-sec/src/permission.rs b/gix-sec/src/permission.rs new file mode 100644 index 0000000..e69de29 diff --git a/gix-sec/src/trust.rs b/gix-sec/src/trust.rs new file mode 100644 index 0000000..e69de29 diff --git a/gix-sec/tests/identity/mod.rs b/gix-sec/tests/identity/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/gix-sec/tests/sec.rs b/gix-sec/tests/sec.rs new file mode 100644 index 0000000..e69de29 "#; crate::assert_hash_agnostic_patch_eq( &std::fs::read_to_string(repo_workdir()?.join("baseline-2.no-renames"))?, expected, ); } assert_eq!(out, None, "tracking is disabled completely"); Ok(()) } #[test] fn realistic_renames_disabled_3() -> crate::Result { let (changes, out) = collect_changes_opts( "r3-base", "r3-change", Options { location: Some(Location::Path), rewrites: None, }, )?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&(changes.into_iter().filter(|c| !c.entry_mode().is_tree()).collect::>())).0, @r#" [ Addition { location: "src/ein.rs", relation: None, entry_mode: EntryMode(0o100644), id: Oid(1), }, Addition { location: "src/gix.rs", relation: None, entry_mode: EntryMode(0o100644), id: Oid(1), }, Deletion { location: "src/plumbing-cli.rs", relation: None, entry_mode: EntryMode(0o100644), id: Oid(1), }, Deletion { location: "src/porcelain-cli.rs", relation: None, entry_mode: EntryMode(0o100644), id: Oid(1), }, ] "#); #[cfg(not(windows))] { let expected = r#"commit 0cf7a4fe3ad6c49ae7beb394a1c1df7cc5173ce4 Author: author Date: Sat Jan 1 00:00:00 2000 +0000 r3-change diff --git a/src/ein.rs b/src/ein.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/gix.rs b/src/gix.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/plumbing-cli.rs b/src/plumbing-cli.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/porcelain-cli.rs b/src/porcelain-cli.rs deleted file mode 100644 index e69de29..0000000 "#; crate::assert_hash_agnostic_patch_eq( &std::fs::read_to_string(repo_workdir()?.join("baseline-3.no-renames"))?, expected, ); } assert_eq!(out, None, "tracking is disabled completely"); Ok(()) } #[test] fn realistic_renames_by_identity_3() -> crate::Result { let (changes, out) = collect_changes_opts( "r3-base", "r3-change", Options { location: Some(Location::Path), rewrites: Some(Rewrites { copies: Some(Copies::default()), limit: 1, track_empty: true, ..Default::default() }), }, )?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&(changes.into_iter().filter(|c| !c.entry_mode().is_tree()).collect::>())).0, @r#" [ Rewrite { source_location: "src/plumbing-cli.rs", source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Oid(1), diff: None, entry_mode: EntryMode(0o100644), id: Oid(1), location: "src/ein.rs", relation: None, copy: false, }, Rewrite { source_location: "src/porcelain-cli.rs", source_entry_mode: EntryMode(0o100644), source_relation: None, source_id: Oid(1), diff: None, entry_mode: EntryMode(0o100644), id: Oid(1), location: "src/gix.rs", relation: None, copy: false, }, ] "#); #[cfg(not(windows))] { let expected = r#"commit 0cf7a4fe3ad6c49ae7beb394a1c1df7cc5173ce4 Author: author Date: Sat Jan 1 00:00:00 2000 +0000 r3-change diff --git a/src/plumbing-cli.rs b/src/ein.rs similarity index 100% rename from src/plumbing-cli.rs rename to src/ein.rs diff --git a/src/porcelain-cli.rs b/src/gix.rs similarity index 100% rename from src/porcelain-cli.rs rename to src/gix.rs "#; crate::assert_hash_agnostic_patch_eq( &std::fs::read_to_string(repo_workdir()?.join("baseline-3.with-renames"))?, expected, ); } let out = out.expect("tracking enabled"); assert_eq!( out.num_similarity_checks, 0, "similarity checks disabled, and not necessary" ); assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); Ok(()) } #[test] fn realistic_renames_2() -> crate::Result { let (changes, out) = collect_changes_opts( "r2-base", "r2-change", Options { location: Some(Location::Path), rewrites: Some(Rewrites { copies: Some(Copies::default()), limit: 1, track_empty: false, ..Default::default() }), }, )?; // Look how nicely it captures and associates this directory rename. insta::assert_snapshot!(crate::normalize_debug_snapshot(&(changes.into_iter() .filter(|c| !c.entry_mode().is_tree() || c.relation().is_some_and(|r| matches!(r, Relation::Parent(_))) ).collect::>())).0, @r#" [ Rewrite { source_location: "git-sec", source_entry_mode: EntryMode(0o40000), source_relation: Some( Parent( 1, ), ), source_id: Oid(1), diff: None, entry_mode: EntryMode(0o40000), id: Oid(1), location: "gix-sec", relation: Some( Parent( 2, ), ), copy: false, }, Rewrite { source_location: "git-sec/CHANGELOG.md", source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 1, ), ), source_id: Oid(2), diff: None, entry_mode: EntryMode(0o100644), id: Oid(2), location: "gix-sec/CHANGELOG.md", relation: Some( ChildOfParent( 2, ), ), copy: false, }, Rewrite { source_location: "git-sec/Cargo.toml", source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 1, ), ), source_id: Oid(2), diff: None, entry_mode: EntryMode(0o100644), id: Oid(2), location: "gix-sec/Cargo.toml", relation: Some( ChildOfParent( 2, ), ), copy: false, }, Rewrite { source_location: "git-sec/src/identity.rs", source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 1, ), ), source_id: Oid(2), diff: None, entry_mode: EntryMode(0o100644), id: Oid(2), location: "gix-sec/src/identity.rs", relation: Some( ChildOfParent( 2, ), ), copy: false, }, Rewrite { source_location: "git-sec/src/lib.rs", source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 1, ), ), source_id: Oid(2), diff: None, entry_mode: EntryMode(0o100644), id: Oid(2), location: "gix-sec/src/lib.rs", relation: Some( ChildOfParent( 2, ), ), copy: false, }, Rewrite { source_location: "git-sec/src/permission.rs", source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 1, ), ), source_id: Oid(2), diff: None, entry_mode: EntryMode(0o100644), id: Oid(2), location: "gix-sec/src/permission.rs", relation: Some( ChildOfParent( 2, ), ), copy: false, }, Rewrite { source_location: "git-sec/src/trust.rs", source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 1, ), ), source_id: Oid(2), diff: None, entry_mode: EntryMode(0o100644), id: Oid(2), location: "gix-sec/src/trust.rs", relation: Some( ChildOfParent( 2, ), ), copy: false, }, Rewrite { source_location: "git-sec/tests/identity/mod.rs", source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 1, ), ), source_id: Oid(2), diff: None, entry_mode: EntryMode(0o100644), id: Oid(2), location: "gix-sec/tests/identity/mod.rs", relation: Some( ChildOfParent( 2, ), ), copy: false, }, Rewrite { source_location: "git-sec/tests/sec.rs", source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 1, ), ), source_id: Oid(2), diff: None, entry_mode: EntryMode(0o100644), id: Oid(2), location: "gix-sec/tests/sec.rs", relation: Some( ChildOfParent( 2, ), ), copy: false, }, ] "#); #[cfg(not(windows))] { let expected = r#"commit d78c63c5ea3149040767e4387e7fc743cda118fd Author: author Date: Sat Jan 1 00:00:00 2000 +0000 r2-change diff --git a/git-sec/CHANGELOG.md b/gix-sec/CHANGELOG.md similarity index 100% rename from git-sec/CHANGELOG.md rename to gix-sec/CHANGELOG.md diff --git a/git-sec/Cargo.toml b/gix-sec/Cargo.toml similarity index 100% rename from git-sec/Cargo.toml rename to gix-sec/Cargo.toml diff --git a/git-sec/src/identity.rs b/gix-sec/src/identity.rs similarity index 100% rename from git-sec/src/identity.rs rename to gix-sec/src/identity.rs diff --git a/git-sec/src/lib.rs b/gix-sec/src/lib.rs similarity index 100% rename from git-sec/src/lib.rs rename to gix-sec/src/lib.rs diff --git a/git-sec/src/permission.rs b/gix-sec/src/permission.rs similarity index 100% rename from git-sec/src/permission.rs rename to gix-sec/src/permission.rs diff --git a/git-sec/src/trust.rs b/gix-sec/src/trust.rs similarity index 100% rename from git-sec/src/trust.rs rename to gix-sec/src/trust.rs diff --git a/git-sec/tests/identity/mod.rs b/gix-sec/tests/identity/mod.rs similarity index 100% rename from git-sec/tests/identity/mod.rs rename to gix-sec/tests/identity/mod.rs diff --git a/git-sec/tests/sec.rs b/gix-sec/tests/sec.rs similarity index 100% rename from git-sec/tests/sec.rs rename to gix-sec/tests/sec.rs "#; crate::assert_hash_agnostic_patch_eq( &std::fs::read_to_string(repo_workdir()?.join("baseline-2.with-renames"))?, expected, ); } let out = out.expect("tracking enabled"); assert_eq!( out.num_similarity_checks, 0, "similarity checks disabled, and not necessary" ); assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); Ok(()) } #[test] fn realistic_renames_3_without_identity() -> crate::Result { let (changes, out) = collect_changes_opts( "r4-base", "r4-dir-rename-non-identity", Options { location: Some(Location::Path), rewrites: Some(Rewrites { copies: None, percentage: None, limit: 0, track_empty: false, }), }, )?; // The same rewrites are found in both modes, but their emitted order follows object-id ordering. match crate::fixture_hash_kind() { gix_hash::Kind::Sha1 => { insta::assert_snapshot!(crate::normalize_debug_snapshot(&( changes .into_iter() .filter(|c| !c.entry_mode().is_tree() || c.relation().is_some_and(|r| matches!(r, Relation::Parent(_)))) .collect::>())).0, @r#" [ Rewrite { source_location: "src/plumbing/options.rs", source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, ), ), source_id: Oid(1), diff: None, entry_mode: EntryMode(0o100644), id: Oid(1), location: "src/plumbing-renamed/options/mod.rs", relation: Some( ChildOfParent( 1, ), ), copy: false, }, Rewrite { source_location: "src/plumbing/mod.rs", source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, ), ), source_id: Oid(2), diff: None, entry_mode: EntryMode(0o100644), id: Oid(2), location: "src/plumbing-renamed/mod.rs", relation: Some( ChildOfParent( 1, ), ), copy: false, }, Rewrite { source_location: "src/plumbing/main.rs", source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, ), ), source_id: Oid(3), diff: None, entry_mode: EntryMode(0o100644), id: Oid(3), location: "src/plumbing-renamed/main.rs", relation: Some( ChildOfParent( 1, ), ), copy: false, }, Rewrite { source_location: "src/plumbing", source_entry_mode: EntryMode(0o40000), source_relation: Some( Parent( 2, ), ), source_id: Oid(4), diff: None, entry_mode: EntryMode(0o40000), id: Oid(5), location: "src/plumbing-renamed", relation: Some( Parent( 1, ), ), copy: false, }, ] "# ); } gix_hash::Kind::Sha256 => { insta::assert_snapshot!(crate::normalize_debug_snapshot(&( changes .into_iter() .filter(|c| !c.entry_mode().is_tree() || c.relation().is_some_and(|r| matches!(r, Relation::Parent(_)))) .collect::>())).0, @r#" [ Rewrite { source_location: "src/plumbing/mod.rs", source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, ), ), source_id: Oid(1), diff: None, entry_mode: EntryMode(0o100644), id: Oid(1), location: "src/plumbing-renamed/mod.rs", relation: Some( ChildOfParent( 1, ), ), copy: false, }, Rewrite { source_location: "src/plumbing/main.rs", source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, ), ), source_id: Oid(2), diff: None, entry_mode: EntryMode(0o100644), id: Oid(2), location: "src/plumbing-renamed/main.rs", relation: Some( ChildOfParent( 1, ), ), copy: false, }, Rewrite { source_location: "src/plumbing/options.rs", source_entry_mode: EntryMode(0o100644), source_relation: Some( ChildOfParent( 2, ), ), source_id: Oid(3), diff: None, entry_mode: EntryMode(0o100644), id: Oid(3), location: "src/plumbing-renamed/options/mod.rs", relation: Some( ChildOfParent( 1, ), ), copy: false, }, Rewrite { source_location: "src/plumbing", source_entry_mode: EntryMode(0o40000), source_relation: Some( Parent( 2, ), ), source_id: Oid(4), diff: None, entry_mode: EntryMode(0o40000), id: Oid(5), location: "src/plumbing-renamed", relation: Some( Parent( 1, ), ), copy: false, }, ] "# ); } _ => unreachable!("tests only support sha1 and sha256 fixtures"), } let out = out.expect("tracking enabled"); assert_eq!( out.num_similarity_checks, 0, "similarity checks disabled, and not necessary" ); assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); Ok(()) } mod util { use std::{ convert::Infallible, path::{Path, PathBuf}, }; use gix_diff::rewrites; use gix_object::{FindExt, TreeRefIter}; pub fn repo_workdir() -> crate::Result { crate::scripted_fixture_read_only("make_diff_for_rewrites_repo.sh") } pub fn fixture_hash_kind() -> gix_hash::Kind { gix_testtools::object_hash() } pub fn repo_with_trees( lhs: impl Into>, rhs: impl Into>, ) -> gix_testtools::Result<(Vec, Vec, gix_diff::blob::Platform, gix_odb::Handle)> { let root = repo_workdir()?; let odb = gix_odb::at_opts( root.join(".git/objects"), Vec::new(), gix_odb::store::init::Options { object_hash: fixture_hash_kind(), ..Default::default() }, )?; let lhs = read_tree(&odb, &root, lhs.into())?; let rhs = read_tree(&odb, &root, rhs.into())?; let cache = gix_diff::blob::Platform::new( Default::default(), gix_diff::blob::Pipeline::new(Default::default(), Default::default(), Vec::new(), Default::default()), Default::default(), gix_worktree::Stack::new( &root, gix_worktree::stack::State::AttributesStack(gix_worktree::stack::state::Attributes::default()), Default::default(), Vec::new(), Vec::new(), ), ); Ok((lhs, rhs, cache, odb)) } pub fn collect_changes( lhs: impl Into>, rhs: impl Into>, ) -> gix_testtools::Result<(Vec, Option)> { let options = gix_diff::tree_with_rewrites::Options { location: Some(gix_diff::tree::recorder::Location::Path), rewrites: None, }; collect_changes_opts(lhs, rhs, options) } pub fn collect_changes_opts( lhs: impl Into>, rhs: impl Into>, options: gix_diff::tree_with_rewrites::Options, ) -> gix_testtools::Result<(Vec, Option)> { let (from, to, mut cache, odb) = repo_with_trees(lhs, rhs)?; let mut out = Vec::new(); let hash_kind = fixture_hash_kind(); let rewrites_info = gix_diff::tree_with_rewrites( TreeRefIter::from_bytes(&from, hash_kind), TreeRefIter::from_bytes(&to, hash_kind), &mut cache, &mut Default::default(), &odb, |change| -> Result<_, Infallible> { out.push(change.into_owned()); Ok(std::ops::ControlFlow::Continue(())) }, options, )?; Ok((out, rewrites_info)) } fn read_tree(odb: &dyn gix_object::Find, root: &Path, tree: Option<&str>) -> gix_testtools::Result> { let Some(tree) = tree else { return Ok(Vec::new()) }; let tree_id_path = root.join(tree).with_extension("tree"); let hex_id = std::fs::read_to_string(&tree_id_path) .map_err(|err| std::io::Error::other(format!("Could not read '{}': {}", tree_id_path.display(), err)))?; let tree_id = gix_hash::ObjectId::from_hex(hex_id.trim().as_bytes())?; let mut buf = Vec::new(); odb.find_tree(&tree_id, &mut buf)?; Ok(buf) } } use util::{collect_changes, collect_changes_opts, repo_with_trees, repo_workdir};