From 08f22e5dcbde4fedcca70b898f0ec79c7e020906 Mon Sep 17 00:00:00 2001 From: AaronMoat <2937187+AaronMoat@users.noreply.github.com> Date: Sat, 9 May 2026 21:51:59 +1000 Subject: [PATCH 1/6] fix(gix-index): correctly decode untracked extension - Fix ctime/mtime flip in stat decoding - Fix incomplete decoding to mirror git, with associated ported git test Co-authored-by: GPT 5.4 Co-authored-by: Claude Sonnet 4.6 --- gix-index/src/decode/mod.rs | 4 +- gix-index/src/extension/mod.rs | 32 +++++++ gix-index/src/extension/untracked_cache.rs | 84 ++++++++++++++---- .../untracked_cache_empty.tar | Bin 0 -> 57344 bytes .../untracked_cache_populated.tar | Bin 0 -> 57344 bytes .../make_index/untracked_cache_empty.sh | 12 +++ .../make_index/untracked_cache_populated.sh | 5 ++ gix-index/tests/index/file/read.rs | 53 +++++++++++ 8 files changed, 171 insertions(+), 19 deletions(-) create mode 100644 gix-index/tests/fixtures/generated-archives/untracked_cache_empty.tar create mode 100644 gix-index/tests/fixtures/generated-archives/untracked_cache_populated.tar create mode 100755 gix-index/tests/fixtures/make_index/untracked_cache_empty.sh create mode 100755 gix-index/tests/fixtures/make_index/untracked_cache_populated.sh diff --git a/gix-index/src/decode/mod.rs b/gix-index/src/decode/mod.rs index c26160a20b8..2d24dc9524f 100644 --- a/gix-index/src/decode/mod.rs +++ b/gix-index/src/decode/mod.rs @@ -336,11 +336,11 @@ pub(crate) fn stat(data: &[u8]) -> Option<(entry::Stat, &[u8])> { let (size, data) = read_u32(data)?; Some(( entry::Stat { - mtime: entry::stat::Time { + ctime: entry::stat::Time { secs: ctime_secs, nsecs: ctime_nsecs, }, - ctime: entry::stat::Time { + mtime: entry::stat::Time { secs: mtime_secs, nsecs: mtime_nsecs, }, diff --git a/gix-index/src/extension/mod.rs b/gix-index/src/extension/mod.rs index 764274e7d27..ebf7ffb6f27 100644 --- a/gix-index/src/extension/mod.rs +++ b/gix-index/src/extension/mod.rs @@ -62,6 +62,38 @@ pub struct UntrackedCache { directories: Vec, } +impl UntrackedCache { + /// Something identifying the location and machine that this cache is for. + pub fn identifier(&self) -> &bstr::BStr { + self.identifier.as_ref() + } + + /// Stat and object id for the `.git/info/exclude` file, if available. + pub fn info_exclude(&self) -> Option<&untracked_cache::OidStat> { + self.info_exclude.as_ref() + } + + /// Stat and object id for the `core.excludesfile`, if available. + pub fn excludes_file(&self) -> Option<&untracked_cache::OidStat> { + self.excludes_file.as_ref() + } + + /// Usually `.gitignore`. + pub fn exclude_filename_per_dir(&self) -> &bstr::BStr { + self.exclude_filename_per_dir.as_ref() + } + + /// The directory flags Git used while populating the cache. + pub fn dir_flags(&self) -> u32 { + self.dir_flags + } + + /// A list of directories and sub-directories, with `directories[0]` being the root. + pub fn directories(&self) -> &[untracked_cache::Directory] { + &self.directories + } +} + /// The extension for keeping state on recent information provided by the filesystem monitor. #[allow(dead_code)] #[derive(Clone)] diff --git a/gix-index/src/extension/untracked_cache.rs b/gix-index/src/extension/untracked_cache.rs index 27747e61716..b99a3afb0a5 100644 --- a/gix-index/src/extension/untracked_cache.rs +++ b/gix-index/src/extension/untracked_cache.rs @@ -8,7 +8,7 @@ use crate::{ }; /// A structure to track filesystem stat information along with an object id, linking a worktree file with what's in our ODB. -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct OidStat { /// The file system stat information pub stat: entry::Stat, @@ -16,8 +16,20 @@ pub struct OidStat { pub id: ObjectId, } +impl OidStat { + /// The file system stat information. + pub fn stat(&self) -> &entry::Stat { + &self.stat + } + + /// The id of the file in our ODB. + pub fn id(&self) -> ObjectId { + self.id + } +} + /// A directory with information about its untracked files, and its sub-directories -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Directory { /// The directories name, or an empty string if this is the root directory. pub name: BString, @@ -34,10 +46,41 @@ pub struct Directory { pub check_only: bool, } +impl Directory { + /// The directory name, or an empty string if this is the root directory. + pub fn name(&self) -> &bstr::BStr { + self.name.as_ref() + } + + /// Untracked files and directory names. + pub fn untracked_entries(&self) -> &[BString] { + &self.untracked_entries + } + + /// Indices for sub-directories similar to this one. + pub fn sub_directories(&self) -> &[usize] { + &self.sub_directories + } + + /// The directory stat data, if available and valid. + pub fn stat(&self) -> Option<&entry::Stat> { + self.stat.as_ref() + } + + /// The oid of a `.gitignore` file, if it exists. + pub fn exclude_file_oid(&self) -> Option { + self.exclude_file_oid + } + + /// Whether Git marked this directory as check-only. + pub fn check_only(&self) -> bool { + self.check_only + } +} + /// Only used as an indicator pub const SIGNATURE: Signature = *b"UNTR"; -// #[allow(unused)] /// Decode an untracked cache extension from `data`, assuming object hashes are of type `object_hash`. pub fn decode(data: &[u8], object_hash: gix_hash::Kind, alloc_limit_bytes: Option) -> Option { if data.last().is_none_or(|b| *b != 0) { @@ -46,10 +89,29 @@ pub fn decode(data: &[u8], object_hash: gix_hash::Kind, alloc_limit_bytes: Optio let (identifier_len, data) = var_int(data)?; let (identifier, data) = data.split_at_checked(identifier_len.try_into().ok()?)?; + // The on-disk layout matches git's `ondisk_untracked_cache` struct + // https://github.com/git/git/blob/2855562ca6a9c6b0e7bc780b050c1e83c9fcfbd0/dir.c#L3582-L3586 + // https://github.com/git/git/blob/2855562ca6a9c6b0e7bc780b050c1e83c9fcfbd0/dir.c#L3668-L3722 + // info_exclude_stat (36 bytes) + // excludes_file_stat (36 bytes) + // dir_flags ( 4 bytes) + // info_exclude hash (hash_len bytes) + // excludes_file hash (hash_len bytes) + // exclude_per_dir (NUL-terminated) let hash_len = object_hash.len_in_bytes(); - let (info_exclude, data) = decode_oid_stat(data, hash_len)?; - let (excludes_file, data) = decode_oid_stat(data, hash_len)?; + let (info_exclude_stat, data) = crate::decode::stat(data)?; + let (excludes_file_stat, data) = crate::decode::stat(data)?; let (dir_flags, data) = read_u32(data)?; + let (info_exclude_hash, data) = data.split_at_checked(hash_len)?; + let (excludes_file_hash, data) = data.split_at_checked(hash_len)?; + let info_exclude = OidStat { + stat: info_exclude_stat, + id: ObjectId::from_bytes_or_panic(info_exclude_hash), + }; + let excludes_file = OidStat { + stat: excludes_file_stat, + id: ObjectId::from_bytes_or_panic(excludes_file_hash), + }; let (exclude_filename_per_dir, data) = split_at_byte_exclusive(data, 0)?; let (num_directory_blocks, data) = var_int(data)?; @@ -174,15 +236,3 @@ fn decode_directory_block<'a>( data.into() } - -fn decode_oid_stat(data: &[u8], hash_len: usize) -> Option<(OidStat, &[u8])> { - let (stat, data) = crate::decode::stat(data)?; - let (hash, data) = data.split_at_checked(hash_len)?; - Some(( - OidStat { - stat, - id: ObjectId::from_bytes_or_panic(hash), - }, - data, - )) -} diff --git a/gix-index/tests/fixtures/generated-archives/untracked_cache_empty.tar b/gix-index/tests/fixtures/generated-archives/untracked_cache_empty.tar new file mode 100644 index 0000000000000000000000000000000000000000..2dbf071a230784c189b9c116624d0b173f42476e GIT binary patch literal 57344 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 J_${Hp{|8vuliL6Q literal 0 HcmV?d00001 diff --git a/gix-index/tests/fixtures/generated-archives/untracked_cache_populated.tar b/gix-index/tests/fixtures/generated-archives/untracked_cache_populated.tar new file mode 100644 index 0000000000000000000000000000000000000000..a12edf580ab72ac9ce885a2786c60d7b81aed3f2 GIT binary patch literal 57344 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 a%S)xoH2UvMgiT*gF)+oz6a&8|82JB.git/info/exclude +git update-index --untracked-cache diff --git a/gix-index/tests/fixtures/make_index/untracked_cache_populated.sh b/gix-index/tests/fixtures/make_index/untracked_cache_populated.sh new file mode 100755 index 00000000000..91d50f567b0 --- /dev/null +++ b/gix-index/tests/fixtures/make_index/untracked_cache_populated.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +. "$(dirname -- "${BASH_SOURCE[0]}")/untracked_cache_empty.sh" +git status > /dev/null diff --git a/gix-index/tests/index/file/read.rs b/gix-index/tests/index/file/read.rs index 88c074858bc..2f1d0b63d4f 100644 --- a/gix-index/tests/index/file/read.rs +++ b/gix-index/tests/index/file/read.rs @@ -199,6 +199,59 @@ fn untr_extension_with_oids() { assert!(file.untracked().is_some()); } +#[test] +fn untr_extension_empty() { + let file = file("untracked_cache_empty"); + let untracked = file.untracked().expect("untracked cache extension present"); + + assert_eq!(untracked.info_exclude(), None); + assert_eq!(untracked.excludes_file(), None); + assert_eq!(untracked.exclude_filename_per_dir(), ".gitignore"); + assert_eq!(untracked.dir_flags(), 6); + assert!(untracked.directories().is_empty()); +} + +#[test] +fn untr_extension_populated() { + let file = file("untracked_cache_populated"); + let untracked = file.untracked().expect("untracked cache extension present"); + + dbg!(untracked.info_exclude()); + dbg!(untracked.excludes_file()); + + assert_eq!( + untracked.info_exclude().map(|s| s.id), + Some(gix_hash::ObjectId::empty_blob(gix_hash::Kind::Sha1)) + ); + assert_eq!(untracked.excludes_file(), None); + assert_eq!(untracked.exclude_filename_per_dir(), ".gitignore"); + assert_eq!(untracked.dir_flags(), 6); + + let mut dirs = untracked.directories().to_vec(); + dirs.sort_by(|a, b| a.name().cmp(b.name())); + assert_eq!(dirs.len(), 4); + + assert_eq!(dirs[0].name(), ""); + let mut untracked_entries = dirs[0].untracked_entries().to_vec(); + untracked_entries.sort(); + assert_eq!(untracked_entries, vec!["dthree/", "dtwo/", "three"]); + assert_eq!(dirs[0].sub_directories, vec![1, 2, 3]); + + assert_eq!(dirs[1].name(), "done"); + assert!(dirs[1].untracked_entries.is_empty()); + assert!(dirs[1].sub_directories.is_empty()); + + assert_eq!(dirs[2].name(), "dthree"); + assert_eq!(dirs[2].untracked_entries, vec!["three"]); + assert!(dirs[2].sub_directories.is_empty()); + assert!(dirs[2].check_only()); + + assert_eq!(dirs[3].name(), "dtwo"); + assert_eq!(dirs[3].untracked_entries, vec!["two"]); + assert!(dirs[3].sub_directories.is_empty()); + assert!(dirs[3].check_only()); +} + #[test] fn fsmn_v1() { let file = loose_file("FSMN"); From 36d84982d42f5955da3e6288a4da7e152089476a Mon Sep 17 00:00:00 2001 From: "GPT 5.5" Date: Tue, 12 May 2026 19:12:34 +0800 Subject: [PATCH 2/6] feat: allow fixtures to require checked-in archives` Add `scripted_fixture_read_only_needs_archive()` for fixtures whose generated output can vary in byte order across platforms or filesystems. The helper still validates archive identity, but bypasses GIX_TEST_IGNORE_ARCHIVES and re-extracts the checked-in archive so tests can opt into frozen fixture bytes when rerunning the producer is non-deterministic, for instance due to filesystem order. Co-authored-by: Sebastian Thiel --- tests/tools/src/lib.rs | 47 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/tests/tools/src/lib.rs b/tests/tools/src/lib.rs index 9d3141850e9..9f2d0ac86f4 100644 --- a/tests/tools/src/lib.rs +++ b/tests/tools/src/lib.rs @@ -530,6 +530,31 @@ pub fn scripted_fixture_read_only(script_name: impl AsRef) -> Result) } +/// Like [`scripted_fixture_read_only()`], but uses a matching existing archive even if +/// `GIX_TEST_IGNORE_ARCHIVES` is set. +/// +/// Use this only for fixtures whose generated contents are not stable across +/// platforms or filesystems and must therefore be frozen by the checked-in +/// archive. +/// +/// CI normally sets `GIX_TEST_IGNORE_ARCHIVES` so fixture scripts are rerun and +/// tracked archives are proven reproducible. This helper is the opt-out for +/// fixtures where rerunning the producer can legitimately change +/// without changing semantics, for example when Git writes entries in filesystem +/// traversal order. +pub fn scripted_fixture_read_only_needs_archive(script_name: impl AsRef) -> Result { + scripted_fixture_read_only_with_args_inner::) -> PostResult, ()>( + script_name, + None::, + None, + ArgsInHash::Yes, + default_excludes(), + None::<(u32, _)>, + true, + ) + .map(|(dir, _)| dir) +} + /// Run the executable at `script_name`, like `make_repo.sh` to produce a writable directory to which /// the tempdir is returned. It will be removed automatically, courtesy of [`tempfile::TempDir`]. /// @@ -598,6 +623,7 @@ where args_in_hash, excludes, post_process.as_mut().map(|(v, f)| (*v, f)), + false, )?; copy_recursively_into_existing_dir(ro_dir, dst.path())?; (dst, _res_ignored) @@ -611,6 +637,7 @@ where args_in_hash, excludes, post_process.as_mut().map(|(v, f)| (*v, f)), + false, )?; (dst, post_result) } @@ -648,6 +675,7 @@ pub fn scripted_fixture_read_only_with_args( ArgsInHash::Yes, default_excludes(), None::<(u32, _)>, + false, ) .map(|(dir, _)| dir) } @@ -674,6 +702,7 @@ pub fn scripted_fixture_read_only_with_args_single_archive( ArgsInHash::No, default_excludes(), None::<(u32, _)>, + false, ) .map(|(dir, _)| dir) } @@ -698,6 +727,7 @@ pub fn scripted_fixture_read_only_with_post( ArgsInHash::Yes, default_excludes(), Some((version, post_process)), + false, ) .map(|(path, opt)| (path, opt.expect("post_process was provided"))) } @@ -718,6 +748,7 @@ pub fn scripted_fixture_read_only_with_args_with_post( ArgsInHash::Yes, default_excludes(), Some((version, post_process)), + false, ) .map(|(path, opt)| (path, opt.expect("post_process was provided"))) } @@ -738,6 +769,7 @@ pub fn scripted_fixture_read_only_with_args_single_archive_with_post( ArgsInHash::No, default_excludes(), Some((version, post_process)), + false, ) .map(|(path, opt)| (path, opt.expect("post_process was provided"))) } @@ -960,6 +992,7 @@ where &script_result_directory, script_identity, force_run, + false, excludes, &format!("using Rust closure '{name}'"), make_fixture, @@ -1006,11 +1039,13 @@ fn force_and_dir( ) } +#[expect(clippy::too_many_arguments)] fn run_fixture_generator_with_marker_handling( archive_file_path: &Path, script_result_directory: &Path, script_identity: u32, force_run: bool, + needs_archive: bool, excludes: &dyn IsExcluded, description: &str, make_fixture: F, @@ -1029,7 +1064,12 @@ where })?; } std::fs::create_dir_all(script_result_directory)?; - match extract_archive(archive_file_path, script_result_directory, script_identity) { + match extract_archive( + archive_file_path, + script_result_directory, + script_identity, + needs_archive, + ) { Ok((archive_id, platform)) => { eprintln!( "Extracted fixture from archive '{}' ({}, {:?})", @@ -1082,6 +1122,7 @@ fn scripted_fixture_read_only_with_args_inner( args_in_hash: ArgsInHash, excludes: &dyn IsExcluded, post_process: Option<(u32, F)>, + needs_archive: bool, ) -> Result<(PathBuf, Option)> where F: FnMut(FixtureState<'_>) -> PostResult, @@ -1181,6 +1222,7 @@ where &script_result_directory, script_identity_for_archive, force_run, + needs_archive, excludes, &format!("using script '{}'", script_location.display()), |fixture_state| { @@ -1541,12 +1583,13 @@ fn extract_archive( archive: &Path, destination_dir: &Path, required_script_identity: u32, + needs_archive: bool, ) -> std::io::Result<(u32, Option)> { let archive_buf: Vec = { let mut buf = Vec::new(); #[cfg_attr(feature = "xz", allow(unused_mut))] let mut input_archive = std::fs::File::open(archive)?; - if env::var_os("GIX_TEST_IGNORE_ARCHIVES").is_some() { + if !needs_archive && env::var_os("GIX_TEST_IGNORE_ARCHIVES").is_some() { return Err(std::io::Error::other(format!( "Ignoring archive at '{}' as GIX_TEST_IGNORE_ARCHIVES is set.", archive.display() From 7f4d49224f45adb46ecde34a3ef9f9c0379a99a3 Mon Sep 17 00:00:00 2001 From: "GPT 5.5" Date: Tue, 12 May 2026 17:16:51 +0800 Subject: [PATCH 3/6] review Co-authored-by: Sebastian Thiel --- Cargo.lock | 2 + gix-index/Cargo.toml | 1 + gix-index/src/extension/mod.rs | 34 +-- gix-index/src/extension/untracked_cache.rs | 37 ++- .../untracked_cache_empty.tar | Bin 57344 -> 56320 bytes .../untracked_cache_nested.tar | Bin 0 -> 61440 bytes .../untracked_cache_populated.tar | Bin 57344 -> 56320 bytes .../make_index/untracked_cache_empty.sh | 11 +- .../make_index/untracked_cache_nested.sh | 27 ++ .../make_index/untracked_cache_populated.sh | 3 + gix-index/tests/index/file/read.rs | 261 +++++++++++++++--- gix-index/tests/index/main.rs | 8 + 12 files changed, 304 insertions(+), 80 deletions(-) create mode 100644 gix-index/tests/fixtures/generated-archives/untracked_cache_nested.tar create mode 100755 gix-index/tests/fixtures/make_index/untracked_cache_nested.sh diff --git a/Cargo.lock b/Cargo.lock index 2efd9a7cbda..39c40cea82b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1962,6 +1962,7 @@ dependencies = [ "gix-utils", "gix-validate", "hashbrown 0.17.0", + "insta", "itoa", "libc", "memmap2", @@ -3042,6 +3043,7 @@ checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" dependencies = [ "console", "once_cell", + "regex", "similar", "tempfile", ] diff --git a/gix-index/Cargo.toml b/gix-index/Cargo.toml index 61e174af6fa..068cfb27999 100644 --- a/gix-index/Cargo.toml +++ b/gix-index/Cargo.toml @@ -76,6 +76,7 @@ gix-hashtable = { path = "../gix-hashtable" } gix-odb = { path = "../gix-odb" } gix-object = { path = "../gix-object" } filetime = "0.2.27" +insta = { version = "1.46.3", features = ["filters"] } [package.metadata.docs.rs] features = ["sha1", "document-features", "serde"] diff --git a/gix-index/src/extension/mod.rs b/gix-index/src/extension/mod.rs index ebf7ffb6f27..1cf9d4c98a7 100644 --- a/gix-index/src/extension/mod.rs +++ b/gix-index/src/extension/mod.rs @@ -45,7 +45,7 @@ pub struct Link { /// The extension for untracked files. #[allow(dead_code)] -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct UntrackedCache { /// Something identifying the location and machine that this cache is for. /// Should the repository be copied to a different machine, the entire cache can immediately be invalidated. @@ -62,38 +62,6 @@ pub struct UntrackedCache { directories: Vec, } -impl UntrackedCache { - /// Something identifying the location and machine that this cache is for. - pub fn identifier(&self) -> &bstr::BStr { - self.identifier.as_ref() - } - - /// Stat and object id for the `.git/info/exclude` file, if available. - pub fn info_exclude(&self) -> Option<&untracked_cache::OidStat> { - self.info_exclude.as_ref() - } - - /// Stat and object id for the `core.excludesfile`, if available. - pub fn excludes_file(&self) -> Option<&untracked_cache::OidStat> { - self.excludes_file.as_ref() - } - - /// Usually `.gitignore`. - pub fn exclude_filename_per_dir(&self) -> &bstr::BStr { - self.exclude_filename_per_dir.as_ref() - } - - /// The directory flags Git used while populating the cache. - pub fn dir_flags(&self) -> u32 { - self.dir_flags - } - - /// A list of directories and sub-directories, with `directories[0]` being the root. - pub fn directories(&self) -> &[untracked_cache::Directory] { - &self.directories - } -} - /// The extension for keeping state on recent information provided by the filesystem monitor. #[allow(dead_code)] #[derive(Clone)] diff --git a/gix-index/src/extension/untracked_cache.rs b/gix-index/src/extension/untracked_cache.rs index b99a3afb0a5..d72e2e55caa 100644 --- a/gix-index/src/extension/untracked_cache.rs +++ b/gix-index/src/extension/untracked_cache.rs @@ -7,8 +7,40 @@ use crate::{ util::{read_u32, split_at_byte_exclusive, var_int}, }; +impl UntrackedCache { + /// Something identifying the location and machine that this cache is for. + pub fn identifier(&self) -> &bstr::BStr { + self.identifier.as_ref() + } + + /// Stat and object id for the `.git/info/exclude` file, if available. + pub fn info_exclude(&self) -> Option<&OidStat> { + self.info_exclude.as_ref() + } + + /// Stat and object id for the `core.excludesfile`, if available. + pub fn excludes_file(&self) -> Option<&OidStat> { + self.excludes_file.as_ref() + } + + /// Usually `.gitignore`. + pub fn exclude_filename_per_dir(&self) -> &bstr::BStr { + self.exclude_filename_per_dir.as_ref() + } + + /// The directory flags Git used while populating the cache. + pub fn dir_flags(&self) -> u32 { + self.dir_flags + } + + /// A list of directories and sub-directories, with `directories[0]` being the root. + pub fn directories(&self) -> &[Directory] { + &self.directories + } +} + /// A structure to track filesystem stat information along with an object id, linking a worktree file with what's in our ODB. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug)] pub struct OidStat { /// The file system stat information pub stat: entry::Stat, @@ -29,7 +61,7 @@ impl OidStat { } /// A directory with information about its untracked files, and its sub-directories -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug)] pub struct Directory { /// The directories name, or an empty string if this is the root directory. pub name: BString, @@ -48,6 +80,7 @@ pub struct Directory { impl Directory { /// The directory name, or an empty string if this is the root directory. + /// `/` is always used as path-separator. pub fn name(&self) -> &bstr::BStr { self.name.as_ref() } diff --git a/gix-index/tests/fixtures/generated-archives/untracked_cache_empty.tar b/gix-index/tests/fixtures/generated-archives/untracked_cache_empty.tar index 2dbf071a230784c189b9c116624d0b173f42476e..1f9a231a4d3dc0b3af9c519a2bf43c38c4109638 100644 GIT binary patch delta 992 zcmZ{jO=#3W6vs3BQOK^eTdT4yLg%nbi)455FvXg#L z77G;w@uCA>6?+i`!RDZdpof-TJg7*e2SLOh#Diy1tl4DK*4AD!yvOhTC-Z;r&ApD! zJ&!(!B#Et*#x<2wb(<>DVXCI2xQGM>dy%>TR=~k6AQ!@;I)#T$-+>9L#>5s;79<8W zcnZKqn`4HUr^yLkzChKNQXC;EP;F4%JE+`NJgOTSN5Rq=UmRnE_7oSliQ_%R#ogkO zrg%P|55!#%pO1F(NM?8>$vhH}AbR&A-b?Wz!X`nw68YsE@2bBI&U04*!=(0i{a&F) z;)s~u_VR6J85+a*eA+GsXZAz0ZmCgsEEU$-mPI(r4EfIPRKTy>O@6t-5B|%ODT`qW z7cO677;tnhbKWW`M7KZe{F!B<_%^Luh~H`l*be)JV~(stVDH#2VL3PbXt> z1G6Q=Dvj6s5($>oCKUtDLPK%PQn@&#kWzU6D-fdKUBE|E#`OL;P>}zg=&r$H^ijN3b z4sQ4lveZCu%$qEKMO^T1Bi6QQJ?0!!pZ0wI0qCEPH`9{1>-Y2d=61PYtrc{jEDw7I bt5rjTLptGjjCgV7?tI^_LBSAii delta 1816 zcmaJ?O>7%g5Z;ZGCc6}>w5Uo;)lSx%IE}D(?RD&sv{g`BArJ*b(vuxmYwxqY#NOxb zd%;GrXLv{xY(?8~I>Fw7hdiuil2Uo^>)kW@OUBf@el7PZamYEnamA(L#qQ&l)w3B-0r~ z(0z}d_~@<3DLtJPl6pFm)KhxO&;?yjCiTq1z-96gamwC1^VSpJd=G=CCbVv zuYG(bI$=?VX!sR`!2Z?WyMt%n?@r(s30wsK6WG80cx&+N`@Q4)Ek$^f23~@2{e`uc z1wo8$&pb~{Cby{r+I2=;rkSQoojNtS_L560u34|!PQ^=|FeM|ISu_kR$9w<8oyY%q zMi}WkwLl-v6oIGqkV!u`8{HQ=~z%FjF}a}fLBc}N1;hK zYZjEKQ{H3*VilW%S&t=T*(Lm+_&#!T)w{JvX1w^6Twd5VF0V|hGlkjq(t64v|JU4|w>vIv+kC@V;nki+8CxgnE^EJa~;!){=gg6^^4 z7zD`s_)H$;4q*0^L)os8ge+<57V92luA|)yRtMzy2|JncVC z3n$v2LT=g)_7{Rg{(y&cKQP^RD2DNC6r0pSI&6l=(82)^sPXqmprd;{82_>@%cOWOmin@v;bP2ptA__51Q3?dLU)=&2;$GT{aE z!`|qzeZ^jgZ#s4h<@qns`}e0er&157b2^*tW9o@NKPHZ(6TOuSyJ8P^ehq(6nCX9x C5i;fg diff --git a/gix-index/tests/fixtures/generated-archives/untracked_cache_nested.tar b/gix-index/tests/fixtures/generated-archives/untracked_cache_nested.tar new file mode 100644 index 0000000000000000000000000000000000000000..ac41faf24d11d4ccbfd0eee04e18a6ad9f9826ac GIT binary patch literal 61440 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 literal 0 HcmV?d00001 diff --git a/gix-index/tests/fixtures/generated-archives/untracked_cache_populated.tar b/gix-index/tests/fixtures/generated-archives/untracked_cache_populated.tar index a12edf580ab72ac9ce885a2786c60d7b81aed3f2..163fa81a3f23407b2be143bc7b0938467241fc8b 100644 GIT binary patch delta 1331 zcmZ{kOGp(_7{|}KkKn5K=!II~Sy(7F-ZSsYGNczowJ0U*W#gSuN5`4V+__#wOhF-O zR|g8(6uAk4qGr*Nu+2Ok*m2liy6Q>o;(Ymz*Pkx8N$6PL92J3MP)>evCCM2;Q)4d z0^xjBRu3^plVY5BCz0=Saf~@%)ukFrsAR$KRW*eo@bNBBoaZrKQk*Rk=a&>`7mG&% zaZwa~aT~^`qIH7E02*@wjTy|a&bgSTu}FtVq8!!^M1D;*AD!2In{S5zR4=dp{eWm? z>)6_&%8P!MrU2maN7$#H?fQ5k*muv}x1l=RXBvh@8TtbM^^(8u#;JgxbNTjvc`IiC z(0QQaAOMkzlTG`Klw_%fj*^`j#mppoMomLc%F1xEPqq59JuN9?h^#1AM0OL;U|!rT zFjQ7oM>e9&XvR{8&`!zBsk)!>w_Jsnfn$RV&XXJEgA;}8@=|v13>0GS#k0wh0Fwj& zgoQc;fTci@?=^v2SSiGMP_^H*KGkn33OP`*f5OZ3=n*-zLB_2B!0*o4D;GcFeAsd4FgwHLbqR zpRL(_t8#j89W4l@%_cB-p&z?$#uBDtnWJbMqU~$P^+cubwjqopT}Gs~uQSo;#VD%P z9_V`&iD#{J)*@R6Cblkx+&u4UHj9 z%Ic~9?wn+$`iCUFsTqyiw+Hf(Tly5Prv>K$mOKiMW7+UgT>2b^Eu>-g%ra#}wGe$y zo4Rs!ZB3yWf(G#?Mir5-b8(Rm%5(=zMKEAIj=*P!J%r%}|AhY_k6nb1;efE=%lTMU zNjKXy`LB@td2_m!V;7|_iow9L;oC~M0=LkpsXd+66tqXRD4OS+?RM@Vv{C+p{~_sU F_g{p=u(SXG delta 2048 zcmaJ?O>7%Q6yA;7G+s(en-aBE)xNIXxQ(!P{S!N+KMHCqpfofh=>cN9ti5AxiKW0-l6Shufsvl9LsIZ=5x}dXGBa-mN&MUZo?r&^8tH3b4zwOoy%t> zog@14<5wg5CUsp(>$!Yd&*+(~F6nwYtxpz0$gaoaz1GHo`CO^|Pq@mGv~&3E?TcP# z_mS9$No}IxBS}L3@^3GMYk->*xQKvA{3qnEe0Vin1Kd2vO-I}nDo~Pi@x_IgBxz{o z%CYmbYPc4)LA%I^!!*NisNJH5tIavI?ij6>W!JsTC{Z$nr*oO~RDM$T-Wz>VxIS4t zf0mo#YhY@J_x8VVVGph#WTiJRhihQ@*#Sm+Rmd*g+!L+==G?=WG0Vq{S%?|u>_4{|Bz=9OaC7+f zg+@PwL*?kKgX`x3tn#lqzkti0xi-$VoMRREe%B(I{8o+EyKs z$vSb94ylvXHmKz)bx1K4=p3y!45!0f2h-qKF|I%v*;saITw9DCpE(>qQXcP2mjt?v z;%-7YrYfvzF-9GBscG5hemTB2xJ?(5#kHxYid{H06~3a6!F_w~BXv>}s#0kh_L476 zlougdnusTm(=!$`noYU_+!h1o8jg$ih84?gfNEN`8YESq4ye!DCRv>Z6)YPALZCv; zH-OgoJD%gmsz*{V@6t9jDP7{nST$H7NEQJD!fOdrDaF5B{OEv5Rf)!6e#vTMltlEH zNCV+{;Gf|N+rrJjIMl2rNhwh+zQ%f+nd@qo#coI*H`vuou{U5t?8IOJZ&b4!=0e+W z4D15!26E7BLWQ6{@vSabUL#c(8pJS(1B`NiO^USH)CHRmvj;9k4X8Fqb%~hh)if|) zONrXPkB@S3vpa_?1Ca1VUyb4{zvN66O4DkZ@>3*oXF)0T63lxL2igZ{E)6m2M7u4`GQV4W*i}J5oib7Vg z*yGp^yk}0#zW>PqKG;U?4K_WW9}KpwqG|i${bH=4IOs+n$g=Fg$eHVK2ql?qdZCcc z_`QzL#35>XhYy|}Nb~tk(g=2cU+j-lJ-f;3wqVrxfY7dfpWW?&-R!WQN#iv22kmzo zTXqg6Q*zn1R{Oi|{3(0?{&a0`=ArJKD-;5s`tr{^.git/info/exclude git update-index --untracked-cache diff --git a/gix-index/tests/fixtures/make_index/untracked_cache_nested.sh b/gix-index/tests/fixtures/make_index/untracked_cache_nested.sh new file mode 100755 index 00000000000..9724fa621cc --- /dev/null +++ b/gix-index/tests/fixtures/make_index/untracked_cache_nested.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +GIT_FORCE_UNTRACKED_CACHE=true +export GIT_FORCE_UNTRACKED_CACHE + +git init -q +git config core.excludesFile "" + +# This fixture extends the populated case with a tracked per-directory +# `.gitignore` and nested untracked directories. The path names are intentionally +# verbose because the test snapshots assert the decoded UNTR directory graph. +mkdir -p tracked-dir-with-ignore/nested-untracked-dir/deep-untracked-dir \ + untracked-dir-2 \ + untracked-dir-3 +touch tracked-root-one tracked-root-two untracked-root-file \ + tracked-dir-with-ignore/tracked-file \ + tracked-dir-with-ignore/visible-untracked-file \ + tracked-dir-with-ignore/nested-untracked-dir/deep-untracked-dir/deep-untracked-file \ + untracked-dir-2/untracked-file-two \ + untracked-dir-3/untracked-file-three +printf "ignored-by-dir-ignore\nalso-ignored-by-dir-ignore\n" >tracked-dir-with-ignore/.gitignore +git add tracked-root-one tracked-root-two tracked-dir-with-ignore/tracked-file tracked-dir-with-ignore/.gitignore +mkdir -p .git/info +: >.git/info/exclude +git update-index --untracked-cache +git status --porcelain >/dev/null diff --git a/gix-index/tests/fixtures/make_index/untracked_cache_populated.sh b/gix-index/tests/fixtures/make_index/untracked_cache_populated.sh index 91d50f567b0..8df1941db09 100755 --- a/gix-index/tests/fixtures/make_index/untracked_cache_populated.sh +++ b/gix-index/tests/fixtures/make_index/untracked_cache_populated.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash set -eu -o pipefail +# Reuse the empty UNTR fixture and run status so Git fills the cache with the +# descriptive tracked/untracked path layout from that script. . "$(dirname -- "${BASH_SOURCE[0]}")/untracked_cache_empty.sh" +# This triggers the untracked cache to be refreshed. git status > /dev/null diff --git a/gix-index/tests/index/file/read.rs b/gix-index/tests/index/file/read.rs index 2f1d0b63d4f..e517e3cc5d7 100644 --- a/gix-index/tests/index/file/read.rs +++ b/gix-index/tests/index/file/read.rs @@ -32,11 +32,35 @@ pub(crate) fn try_file(name: &str) -> Result gix_index::File { try_file(name).unwrap() } +/// Needed if we have to freeze the fixture if contents depends on filesystem traversal order +/// This is Ok and similar to our manual copies of indices, except that it can be regenerated. +fn file_needs_archive(name: &str) -> gix_index::File { + let file = gix_index::File::at( + crate::fixture_index_path_needs_archive(name), + gix_hash::Kind::Sha1, + false, + Default::default(), + ) + .unwrap(); + verify(file) +} fn file_opt(name: &str, opts: gix_index::decode::Options) -> gix_index::File { let file = gix_index::File::at(crate::fixture_index_path(name), gix_hash::Kind::Sha1, false, opts).unwrap(); verify(file) } +fn with_untracked_snapshot_filters(run: impl FnOnce()) { + let mut settings = insta::Settings::clone_current(); + settings.set_filters(vec![ + (r#"(identifier: )"[^"]*""#, r#"$1"[redacted]""#), + ( + r"(?s)Stat \{\s+mtime: Time \{\s+secs: \d+,\s+nsecs: \d+,\s+\},\s+ctime: Time \{\s+secs: \d+,\s+nsecs: \d+,\s+\},\s+dev: \d+,\s+ino: \d+,\s+uid: \d+,\s+gid: \d+,\s+size: \d+,\s+\}", + "Stat { ... }", + ), + ]); + settings.bind(run); +} + #[test] fn v2_with_single_entry_tree_and_eoie_ext() { let file_disallow_threaded_loading = file_opt( @@ -201,55 +225,212 @@ fn untr_extension_with_oids() { #[test] fn untr_extension_empty() { - let file = file("untracked_cache_empty"); + let file = file_needs_archive("untracked_cache_empty"); let untracked = file.untracked().expect("untracked cache extension present"); - assert_eq!(untracked.info_exclude(), None); - assert_eq!(untracked.excludes_file(), None); - assert_eq!(untracked.exclude_filename_per_dir(), ".gitignore"); - assert_eq!(untracked.dir_flags(), 6); - assert!(untracked.directories().is_empty()); + with_untracked_snapshot_filters(|| { + insta::assert_debug_snapshot!(untracked, @r#" + UntrackedCache { + identifier: "[redacted]", + info_exclude: None, + excludes_file: None, + exclude_filename_per_dir: ".gitignore", + dir_flags: 6, + directories: [], + } + "#); + }); } #[test] fn untr_extension_populated() { - let file = file("untracked_cache_populated"); + let file = file_needs_archive("untracked_cache_populated"); let untracked = file.untracked().expect("untracked cache extension present"); - dbg!(untracked.info_exclude()); - dbg!(untracked.excludes_file()); + with_untracked_snapshot_filters(|| { + insta::assert_debug_snapshot!(untracked, @r#" + UntrackedCache { + identifier: "[redacted]", + info_exclude: Some( + OidStat { + stat: Stat { ... }, + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + ), + excludes_file: None, + exclude_filename_per_dir: ".gitignore", + dir_flags: 6, + directories: [ + Directory { + name: "", + untracked_entries: [ + "untracked-root-file", + "untracked-dir-3/", + "untracked-dir-2/", + ], + sub_directories: [ + 1, + 2, + 3, + ], + stat: Some( + Stat { ... }, + ), + exclude_file_oid: None, + check_only: false, + }, + Directory { + name: "tracked-dir", + untracked_entries: [], + sub_directories: [], + stat: Some( + Stat { ... }, + ), + exclude_file_oid: None, + check_only: false, + }, + Directory { + name: "untracked-dir-2", + untracked_entries: [ + "untracked-file-two", + ], + sub_directories: [], + stat: Some( + Stat { ... }, + ), + exclude_file_oid: None, + check_only: true, + }, + Directory { + name: "untracked-dir-3", + untracked_entries: [ + "untracked-file-three", + ], + sub_directories: [], + stat: Some( + Stat { ... }, + ), + exclude_file_oid: None, + check_only: true, + }, + ], + } + "#); + }); +} - assert_eq!( - untracked.info_exclude().map(|s| s.id), - Some(gix_hash::ObjectId::empty_blob(gix_hash::Kind::Sha1)) - ); - assert_eq!(untracked.excludes_file(), None); - assert_eq!(untracked.exclude_filename_per_dir(), ".gitignore"); - assert_eq!(untracked.dir_flags(), 6); - - let mut dirs = untracked.directories().to_vec(); - dirs.sort_by(|a, b| a.name().cmp(b.name())); - assert_eq!(dirs.len(), 4); - - assert_eq!(dirs[0].name(), ""); - let mut untracked_entries = dirs[0].untracked_entries().to_vec(); - untracked_entries.sort(); - assert_eq!(untracked_entries, vec!["dthree/", "dtwo/", "three"]); - assert_eq!(dirs[0].sub_directories, vec![1, 2, 3]); - - assert_eq!(dirs[1].name(), "done"); - assert!(dirs[1].untracked_entries.is_empty()); - assert!(dirs[1].sub_directories.is_empty()); - - assert_eq!(dirs[2].name(), "dthree"); - assert_eq!(dirs[2].untracked_entries, vec!["three"]); - assert!(dirs[2].sub_directories.is_empty()); - assert!(dirs[2].check_only()); - - assert_eq!(dirs[3].name(), "dtwo"); - assert_eq!(dirs[3].untracked_entries, vec!["two"]); - assert!(dirs[3].sub_directories.is_empty()); - assert!(dirs[3].check_only()); +/// This mirrors Git's sparse/subdir untracked-cache coverage: a directory can +/// carry its own exclude-file oid, and nested untracked directories are +/// serialized depth-first while root sub-directory indices still point at +/// the corresponding directory records. +#[test] +fn untr_extension_nested() { + let file = file_needs_archive("untracked_cache_nested"); + let untracked = file.untracked().expect("untracked cache extension present"); + + with_untracked_snapshot_filters(|| { + insta::assert_debug_snapshot!(untracked, @r#" + UntrackedCache { + identifier: "[redacted]", + info_exclude: Some( + OidStat { + stat: Stat { ... }, + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, + ), + excludes_file: None, + exclude_filename_per_dir: ".gitignore", + dir_flags: 6, + directories: [ + Directory { + name: "", + untracked_entries: [ + "untracked-root-file", + "untracked-dir-3/", + "untracked-dir-2/", + ], + sub_directories: [ + 1, + 4, + 5, + ], + stat: Some( + Stat { ... }, + ), + exclude_file_oid: None, + check_only: false, + }, + Directory { + name: "tracked-dir-with-ignore", + untracked_entries: [ + "visible-untracked-file", + "nested-untracked-dir/", + ], + sub_directories: [ + 2, + ], + stat: Some( + Stat { ... }, + ), + exclude_file_oid: Some( + Sha1(55535cdccae965cd0ea191aa22df1145a983b2f9), + ), + check_only: false, + }, + Directory { + name: "nested-untracked-dir", + untracked_entries: [ + "deep-untracked-dir/", + ], + sub_directories: [ + 3, + ], + stat: Some( + Stat { ... }, + ), + exclude_file_oid: None, + check_only: true, + }, + Directory { + name: "deep-untracked-dir", + untracked_entries: [ + "deep-untracked-file", + ], + sub_directories: [], + stat: Some( + Stat { ... }, + ), + exclude_file_oid: None, + check_only: true, + }, + Directory { + name: "untracked-dir-2", + untracked_entries: [ + "untracked-file-two", + ], + sub_directories: [], + stat: Some( + Stat { ... }, + ), + exclude_file_oid: None, + check_only: true, + }, + Directory { + name: "untracked-dir-3", + untracked_entries: [ + "untracked-file-three", + ], + sub_directories: [], + stat: Some( + Stat { ... }, + ), + exclude_file_oid: None, + check_only: true, + }, + ], + } + "#); + }); } #[test] diff --git a/gix-index/tests/index/main.rs b/gix-index/tests/index/main.rs index 2e55ea3ae86..8d2c2157cf3 100644 --- a/gix-index/tests/index/main.rs +++ b/gix-index/tests/index/main.rs @@ -22,6 +22,14 @@ pub fn fixture_index_path(name: &str) -> PathBuf { dir.join(".git").join("index") } +pub fn fixture_index_path_needs_archive(name: &str) -> PathBuf { + let dir = gix_testtools::scripted_fixture_read_only_needs_archive( + Path::new("make_index").join(name).with_extension("sh"), + ) + .expect("script works"); + dir.join(".git").join("index") +} + pub fn loose_file_path(name: &str) -> PathBuf { gix_testtools::fixture_path(Path::new("loose_index").join(name).with_extension("git-index")) } From 83ab103077a76683b97755b3acd59c67b83a2def Mon Sep 17 00:00:00 2001 From: "GPT 5.5" Date: Tue, 12 May 2026 19:32:47 +0800 Subject: [PATCH 4/6] fix: also override XDG_CONFIG_HOME for test-scripts and commands runs. Co-authored-by: Sebastian Thiel --- tests/tools/src/lib.rs | 4 ++++ tests/tools/src/tests.rs | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/tests/tools/src/lib.rs b/tests/tools/src/lib.rs index 9f2d0ac86f4..b456f46cc7a 100644 --- a/tests/tools/src/lib.rs +++ b/tests/tools/src/lib.rs @@ -1401,6 +1401,10 @@ fn configure_command<'a, I: IntoIterator, S: AsRef>( .env_remove("GIT_ASKPASS") .env_remove("SSH_ASKPASS") .env("MSYS", msys_for_git_bash_on_windows) + .env( + "XDG_CONFIG_HOME", + script_result_directory.join(".gix-testtools-xdg-config"), + ) .env("GIT_CONFIG_NOSYSTEM", "1") .env("GIT_CONFIG_GLOBAL", NULL_DEVICE) .env("GIT_TERMINAL_PROMPT", "false") diff --git a/tests/tools/src/tests.rs b/tests/tools/src/tests.rs index 75150284189..1cd63a03ac1 100644 --- a/tests/tools/src/tests.rs +++ b/tests/tools/src/tests.rs @@ -64,6 +64,23 @@ fn configure_command_clears_external_config() { assert_eq!(status, 0, "reading the config should succeed"); } +#[test] +fn configure_command_overrides_xdg_config_home() { + let temp = tempfile::TempDir::new().expect("can create temp dir"); + let mut cmd = std::process::Command::new(GIT_PROGRAM); + cmd.env("XDG_CONFIG_HOME", temp.path().join("external-config")); + configure_command(&mut cmd, gix_hash::Kind::default(), ["--version"], temp.path()); + + let xdg_config_home = cmd + .get_envs() + .find_map(|(key, value)| (key == "XDG_CONFIG_HOME").then_some(value)) + .flatten(); + assert_eq!( + xdg_config_home, + Some(temp.path().join(".gix-testtools-xdg-config").as_os_str()) + ); +} + #[test] #[cfg(windows)] fn bash_program_ok_for_platform() { From 69d644ea5785e48f63976cbd0430898ed5ba5a34 Mon Sep 17 00:00:00 2001 From: "GPT 5.5" Date: Tue, 12 May 2026 19:52:20 +0800 Subject: [PATCH 5/6] Stabilize UNTR fixture stat times Seed the untracked-cache fixture paths with a known mtime before Git populates the UNTR extension. Keeping that mtime visible in the snapshot covers the ctime/mtime decode ordering: if the fields are swapped, the expectation will show the unexpected timestamp. Leave ctime redacted because it is filesystem metadata-change time and varies by platform and run, while still preserving enough snapshot detail to catch the decode-order regression. Co-authored-by: Sebastian Thiel --- .../untracked_cache_empty.tar | Bin 56320 -> 56320 bytes .../untracked_cache_nested.tar | Bin 61440 -> 61440 bytes .../untracked_cache_populated.tar | Bin 56320 -> 56320 bytes gix-index/tests/fixtures/make_index/shared.sh | 7 +++++ .../make_index/untracked_cache_empty.sh | 14 +++++++++ .../make_index/untracked_cache_nested.sh | 19 ++++++++++++ .../make_index/untracked_cache_populated.sh | 2 +- gix-index/tests/index/file/read.rs | 28 +++++++++--------- 8 files changed, 55 insertions(+), 15 deletions(-) create mode 100755 gix-index/tests/fixtures/make_index/shared.sh diff --git a/gix-index/tests/fixtures/generated-archives/untracked_cache_empty.tar b/gix-index/tests/fixtures/generated-archives/untracked_cache_empty.tar index 1f9a231a4d3dc0b3af9c519a2bf43c38c4109638..cba7120d980bf7e449c4fdd7574e4f11dd362f63 100644 GIT binary patch delta 167 zcmZqJ!Q8Nec>}MIIJ2aXh>&A(7MK>*Wn^Fw5RzRKJ-J3mb+V_B3|L;E^)Ogoz}W~Y z9|4rN1j`AkoPfx^UJjLuoP0*;IG3@7siCETiMf&G}MIIMeS^3#r-*Szy{bnvsD)KuGG(@yRtps*^p1WWe$gMe$&H3B4;& z`6EDiOR$`zyZ}V*X%STJ=;Sj($GHp*O^qx~O-;>AC(jk0D)Q9kE<=XNlSe)7FI8-Q l2Heak7CyRJaNjP*MPhsS5o&cy^D--d;_n$Z|GUh@0RW^dJC^_e diff --git a/gix-index/tests/fixtures/generated-archives/untracked_cache_nested.tar b/gix-index/tests/fixtures/generated-archives/untracked_cache_nested.tar index ac41faf24d11d4ccbfd0eee04e18a6ad9f9826ac..616c850f63200f495977cebdd58d84a011b6cc56 100644 GIT binary patch delta 526 zcmZp8z})bFc|)s=goy!zp@E5+p@ETsk+A`Tfq|i+ftfLb!Q@2Nl+BE?JdEPZl0qgm z=ez6w|NqY(eO8o_fk8k>cF~Nf|>0HhV)06C(>t3kyRd-O{|w3LS;w%HopLTm_fJqVmiK0yxsRPmq5VIDQp{iNbjZnjI0Ia4q zVj7y79waph5H)>1$ZC9mf!2$u=-owRML;!u7;5S~&!DO4pZrQmQRKi~50+0xj{+G^ z7+3COo4s!F^8L3b8;U&Gq|$a^a@#3EHWOnrV2pxdaxx?1e}1%p+$?zJJ>%wo&zU#? D`r!Q@2Nl+BE?JdEN@zs@TC zbk)j&(u@oY0zy*XS58)u3k1u3X8#A4`*eaGDz{{E4^-}>krY_&!}+gJxrIPE)yb0b zB4E8A-zbCSKhA4_$}a-TXUa=4w>PLvZj*1bGcqwZwlp^|Hqb52%dF5*D6T9nNzGMo zNh~VQ%wqs)SKg?k3ZtRMR536xeq~@_5Sb{by}3+b6=OZf`JX%=gRKM7P<5p+bwFAH zEcauv1&oHOnS)TnZ~!cK&f+tShN_v1q$U9@_k~pqMuXJ&0K;q^rlOuJ5Jf##pa#vy zQ1exK5?Bq8hN@XG`L&Xw$mW9`rf28z_&vEFksxBSBjxUXXXD9+A`dsIv>gyPLyuJx cV^ecTtWIWR{LhCPsG9}Pyl33}_c=2M0OYi_cK`qY diff --git a/gix-index/tests/fixtures/generated-archives/untracked_cache_populated.tar b/gix-index/tests/fixtures/generated-archives/untracked_cache_populated.tar index 163fa81a3f23407b2be143bc7b0938467241fc8b..4612434cb3e1f360bbb6e938716f17869e5ed3e1 100644 GIT binary patch delta 411 zcmZqJ!Q8Nec|)6kxFLg~fr*)+fsuicu>pgDfuW&+Igp;5$egm7QILmGoLRD!@5L|1 z`v3p`vqzs5Wn^Fw5RzTPF}X%ab+V_B3|Ri<*M(^E%s_cdu-q%T^JsD`lg|j*GtX>L zpUfxR$!TI{YGGn-XkjvWp|BXlw)Uy0I#!ErJ}2zKSPyoJ_GeVdmHG?}j9(cT7({@y zLKaxfK37yVs~;fLFeHH0yfnClrsgrGnn_K_YE*?}S3f~k#BgihzX`K5BGPk~=DXeI zon^3=S8Mu2LHA8!ZM($H&;!=Q*wh>nu#*`X|MS89ty`LxS+QC0(tF0ue=jq00025; Bfg1n- delta 400 zcmZqJ!Q8Nec|)6kxDkV)fr*)+fsuicu>pgDfuW&+xgmqWC~V z=VgKEYmSTz3<5$@fA>zV5mKG(DI^1yk1{?DmXCZA2bJFil(&S)y`K-3i&|d?mD@e} zjF3I^tOkwAe8Qbv#ulbV2BzkgW=4}22#bNWd@SbyYXQri74~4P2fHCE1>%;d z6sV*%0|Vn%1_lNZAgutAyY&-BL)C0Ys9{I|%f)0`!Dy(O9hhofo`XznZR%{l$^qz6^zspP<00qK=EdT%j diff --git a/gix-index/tests/fixtures/make_index/shared.sh b/gix-index/tests/fixtures/make_index/shared.sh new file mode 100755 index 00000000000..bf3d1035607 --- /dev/null +++ b/gix-index/tests/fixtures/make_index/shared.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +UNTRACKED_CACHE_MTIME="2038-01-19 03:14:07.123456789Z" + +seed_untracked_cache_times() { + touch -d "$UNTRACKED_CACHE_MTIME" "$@" +} diff --git a/gix-index/tests/fixtures/make_index/untracked_cache_empty.sh b/gix-index/tests/fixtures/make_index/untracked_cache_empty.sh index aef40b8a1d5..ba9aa208a9b 100755 --- a/gix-index/tests/fixtures/make_index/untracked_cache_empty.sh +++ b/gix-index/tests/fixtures/make_index/untracked_cache_empty.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash set -eu -o pipefail +. "$(dirname -- "${BASH_SOURCE[0]}")/shared.sh" + git init -q mkdir tracked-dir untracked-dir-2 untracked-dir-3 @@ -11,3 +13,15 @@ touch tracked-root-one tracked-root-two untracked-root-file \ git add tracked-root-one tracked-root-two tracked-dir/tracked-file : >.git/info/exclude git update-index --untracked-cache +seed_untracked_cache_times \ + . \ + .git/info/exclude \ + tracked-dir \ + tracked-dir/tracked-file \ + untracked-dir-2 \ + untracked-dir-2/untracked-file-two \ + untracked-dir-3 \ + untracked-dir-3/untracked-file-three \ + tracked-root-one \ + tracked-root-two \ + untracked-root-file diff --git a/gix-index/tests/fixtures/make_index/untracked_cache_nested.sh b/gix-index/tests/fixtures/make_index/untracked_cache_nested.sh index 9724fa621cc..a503e5e1fe3 100755 --- a/gix-index/tests/fixtures/make_index/untracked_cache_nested.sh +++ b/gix-index/tests/fixtures/make_index/untracked_cache_nested.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash set -eu -o pipefail +. "$(dirname -- "${BASH_SOURCE[0]}")/shared.sh" + GIT_FORCE_UNTRACKED_CACHE=true export GIT_FORCE_UNTRACKED_CACHE @@ -24,4 +26,21 @@ git add tracked-root-one tracked-root-two tracked-dir-with-ignore/tracked-file t mkdir -p .git/info : >.git/info/exclude git update-index --untracked-cache +seed_untracked_cache_times \ + . \ + .git/info/exclude \ + tracked-dir-with-ignore \ + tracked-dir-with-ignore/.gitignore \ + tracked-dir-with-ignore/tracked-file \ + tracked-dir-with-ignore/visible-untracked-file \ + tracked-dir-with-ignore/nested-untracked-dir \ + tracked-dir-with-ignore/nested-untracked-dir/deep-untracked-dir \ + tracked-dir-with-ignore/nested-untracked-dir/deep-untracked-dir/deep-untracked-file \ + untracked-dir-2 \ + untracked-dir-2/untracked-file-two \ + untracked-dir-3 \ + untracked-dir-3/untracked-file-three \ + tracked-root-one \ + tracked-root-two \ + untracked-root-file git status --porcelain >/dev/null diff --git a/gix-index/tests/fixtures/make_index/untracked_cache_populated.sh b/gix-index/tests/fixtures/make_index/untracked_cache_populated.sh index 8df1941db09..55c8c1b7d36 100755 --- a/gix-index/tests/fixtures/make_index/untracked_cache_populated.sh +++ b/gix-index/tests/fixtures/make_index/untracked_cache_populated.sh @@ -2,7 +2,7 @@ set -eu -o pipefail # Reuse the empty UNTR fixture and run status so Git fills the cache with the -# descriptive tracked/untracked path layout from that script. +# descriptive tracked/untracked path layout and seeded mtimes from that script. . "$(dirname -- "${BASH_SOURCE[0]}")/untracked_cache_empty.sh" # This triggers the untracked cache to be refreshed. git status > /dev/null diff --git a/gix-index/tests/index/file/read.rs b/gix-index/tests/index/file/read.rs index e517e3cc5d7..5a95e285bf3 100644 --- a/gix-index/tests/index/file/read.rs +++ b/gix-index/tests/index/file/read.rs @@ -54,8 +54,8 @@ fn with_untracked_snapshot_filters(run: impl FnOnce()) { settings.set_filters(vec![ (r#"(identifier: )"[^"]*""#, r#"$1"[redacted]""#), ( - r"(?s)Stat \{\s+mtime: Time \{\s+secs: \d+,\s+nsecs: \d+,\s+\},\s+ctime: Time \{\s+secs: \d+,\s+nsecs: \d+,\s+\},\s+dev: \d+,\s+ino: \d+,\s+uid: \d+,\s+gid: \d+,\s+size: \d+,\s+\}", - "Stat { ... }", + r"(?s)Stat \{\s+mtime: Time \{\s+secs: (\d+),\s+nsecs: (\d+),\s+\},\s+ctime: Time \{\s+secs: (\d+),\s+nsecs: (\d+),\s+\},\s+dev: \d+,\s+ino: \d+,\s+uid: \d+,\s+gid: \d+,\s+size: \d+,\s+\}", + "Stat { mtime: Time { secs: $1, nsecs: $2 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }", ), ]); settings.bind(run); @@ -253,7 +253,7 @@ fn untr_extension_populated() { identifier: "[redacted]", info_exclude: Some( OidStat { - stat: Stat { ... }, + stat: Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, ), @@ -274,7 +274,7 @@ fn untr_extension_populated() { 3, ], stat: Some( - Stat { ... }, + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, ), exclude_file_oid: None, check_only: false, @@ -284,7 +284,7 @@ fn untr_extension_populated() { untracked_entries: [], sub_directories: [], stat: Some( - Stat { ... }, + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, ), exclude_file_oid: None, check_only: false, @@ -296,7 +296,7 @@ fn untr_extension_populated() { ], sub_directories: [], stat: Some( - Stat { ... }, + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, ), exclude_file_oid: None, check_only: true, @@ -308,7 +308,7 @@ fn untr_extension_populated() { ], sub_directories: [], stat: Some( - Stat { ... }, + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, ), exclude_file_oid: None, check_only: true, @@ -334,7 +334,7 @@ fn untr_extension_nested() { identifier: "[redacted]", info_exclude: Some( OidStat { - stat: Stat { ... }, + stat: Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), }, ), @@ -355,7 +355,7 @@ fn untr_extension_nested() { 5, ], stat: Some( - Stat { ... }, + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, ), exclude_file_oid: None, check_only: false, @@ -370,7 +370,7 @@ fn untr_extension_nested() { 2, ], stat: Some( - Stat { ... }, + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, ), exclude_file_oid: Some( Sha1(55535cdccae965cd0ea191aa22df1145a983b2f9), @@ -386,7 +386,7 @@ fn untr_extension_nested() { 3, ], stat: Some( - Stat { ... }, + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, ), exclude_file_oid: None, check_only: true, @@ -398,7 +398,7 @@ fn untr_extension_nested() { ], sub_directories: [], stat: Some( - Stat { ... }, + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, ), exclude_file_oid: None, check_only: true, @@ -410,7 +410,7 @@ fn untr_extension_nested() { ], sub_directories: [], stat: Some( - Stat { ... }, + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, ), exclude_file_oid: None, check_only: true, @@ -422,7 +422,7 @@ fn untr_extension_nested() { ], sub_directories: [], stat: Some( - Stat { ... }, + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, ), exclude_file_oid: None, check_only: true, From e4bdd1fbf9c00853e76cfe34c98990422dc504a9 Mon Sep 17 00:00:00 2001 From: "GPT 5.5" Date: Tue, 12 May 2026 20:50:20 +0800 Subject: [PATCH 6/6] feat(gix-index): greatly improve the index `File` debug output. This allows tests to rely on it more with insta, and not miss a thing. Co-authored-by: Sebastian Thiel --- gix-index/src/file/mod.rs | 49 +- gix-index/src/lib.rs | 51 +- .../untracked_cache_empty.tar | Bin 56320 -> 56320 bytes .../untracked_cache_nested.tar | Bin 61440 -> 61440 bytes gix-index/tests/index/file/read.rs | 545 +++++++++++------- tests/tools/src/lib.rs | 12 +- tests/tools/src/tests.rs | 25 + 7 files changed, 458 insertions(+), 224 deletions(-) diff --git a/gix-index/src/file/mod.rs b/gix-index/src/file/mod.rs index 40332abbd0e..893c18bf8e0 100644 --- a/gix-index/src/file/mod.rs +++ b/gix-index/src/file/mod.rs @@ -21,10 +21,36 @@ mod impls { mod impl_ { use std::fmt::Formatter; - use crate::{File, State}; + use crate::{Entry, File, PathStorageRef, State}; impl std::fmt::Debug for File { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if f.alternate() { + return f + .debug_struct("File") + .field("path", &self.path.display()) + .field("checksum", &self.checksum) + .field("object_hash", &self.state.object_hash) + .field("timestamp", &self.state.timestamp) + .field("version", &self.state.version) + .field( + "entries", + &EntriesDebug { + entries: &self.state.entries, + path_backing: &self.state.path_backing, + }, + ) + .field("path_backing_size_bytes", &self.state.path_backing.len()) + .field("is_sparse", &self.state.is_sparse) + .field("end_of_index_at_decode_time", &self.state.end_of_index_at_decode_time) + .field("offset_table_at_decode_time", &self.state.offset_table_at_decode_time) + .field("tree", &self.state.tree) + .field("has_link", &self.state.link.is_some()) + .field("has_resolve_undo", &self.state.resolve_undo.is_some()) + .field("untracked", &self.state.untracked) + .field("has_fs_monitor", &self.state.fs_monitor.is_some()) + .finish(); + } f.debug_struct("File") .field("path", &self.path.display()) .field("checksum", &self.checksum) @@ -32,6 +58,27 @@ mod impl_ { } } + struct EntriesDebug<'a> { + entries: &'a [Entry], + path_backing: &'a PathStorageRef, + } + + impl std::fmt::Debug for EntriesDebug<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if !f.alternate() { + return f.debug_list().entries(self.entries).finish(); + } + + writeln!(f, "[")?; + for entry in self.entries { + write!(f, " ")?; + entry.fmt_debug(f, Some(self.path_backing))?; + writeln!(f, ",")?; + } + write!(f, "]") + } + } + impl From for State { fn from(f: File) -> Self { f.state diff --git a/gix-index/src/lib.rs b/gix-index/src/lib.rs index f0432775f5c..d29ddccca30 100644 --- a/gix-index/src/lib.rs +++ b/gix-index/src/lib.rs @@ -53,7 +53,7 @@ pub enum Version { } /// An entry in the index, identifying a non-tree item on disk. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Clone, Eq, PartialEq)] pub struct Entry { /// The filesystem stat information for the file on disk. pub stat: entry::Stat, @@ -165,7 +165,54 @@ pub struct State { mod impls { use std::fmt::{Debug, Formatter}; - use crate::{State, entry::Stage}; + use crate::{Entry, PathStorageRef, State, entry::Stage}; + + impl Entry { + pub(crate) fn fmt_debug(&self, f: &mut Formatter, path_backing: Option<&PathStorageRef>) -> std::fmt::Result { + if f.alternate() { + write!( + f, + "{} {}{:?} mtime: {:?} {} ", + match self.flags.stage() { + Stage::Unconflicted => " ", + Stage::Base => "BASE ", + Stage::Ours => "OURS ", + Stage::Theirs => "THEIRS ", + }, + if self.flags.is_empty() { + "".to_string() + } else { + format!("{:?} ", self.flags) + }, + self.mode, + self.stat.mtime, + self.id, + )?; + return match path_backing { + Some(path_backing) => write!(f, "{}", self.path_in(path_backing)), + None => write!(f, "{:?}", self.path), + }; + } + + let mut entry = f.debug_struct("Entry"); + entry + .field("stat", &self.stat) + .field("id", &self.id) + .field("flags", &self.flags) + .field("mode", &self.mode); + match path_backing { + Some(path_backing) => entry.field("path", &self.path_in(path_backing)), + None => entry.field("path", &self.path), + } + .finish() + } + } + + impl Debug for Entry { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.fmt_debug(f, None) + } + } impl Debug for State { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { diff --git a/gix-index/tests/fixtures/generated-archives/untracked_cache_empty.tar b/gix-index/tests/fixtures/generated-archives/untracked_cache_empty.tar index cba7120d980bf7e449c4fdd7574e4f11dd362f63..9d30b35079433eb33540c9a0ed45ca9352db7346 100644 GIT binary patch delta 163 zcmZqJ!Q8Nec>|x2m|{Hl|0g@Mm_hWUc18vU0U^1GOD5L{sZRD3k_AcsvY&=!F{_J7m4lRhby(zEzQfU0E)k7-2Cq{69)jU**cv7 delta 163 zcmZqJ!Q8Nec>|x2n52-1kYjNcGl&+|Wn^Fw5RzRKJ-J3mb+V_BEJ#|Q^)Ogkz}W~Y z9RZZK0!ay~oPbEZUJjLtoP0*;IH$3NsiCETiMi3_Il|LK)bCfDKiN`HH`zEkM(4|u lz@>9TZ*3Obw~KL+*dBhkQcK;^yvz!q_|xEm}0!}fu;5J|NsAIk3K8P$iN^VBscNlWEHs}u+(*BG^sn2dmvJW zV#?5@ZUUuLCriqUf^;5g2}hH@1(wc~Uuw$B@Mga+E_hn*L1;42)kH7#Kt*3TkgIQ&`Pd4^+1Q*C8}z@-SsUS^;8IK|Y$A zD+o0V2f%6$+A*T3xr(GF0iq`TKZ+V}xrx^>6>UsIQ6x9?rJ*-+%cB9%k@XkoTl@XUM0&HtV=aR301d6{4U delta 459 zcmZp8z})bFc>|xEn52+N&H3*7|NsB9N1qjCWMB{wl3g@ovWi>~SnBI(G^xpxdmvJE ztCpZiO#n)%PL`Ax1?jB2Q-mfx5iFf4zu4Z;!ofNVhaEvqDFqxU#q;HCMqU zv8X&Vk0Fa0?s4%P?g?S^;9zqB2x9 zi@FhN7!H8d)J9B0Q`3W_CIOJJPPW gix_index::File { let file = gix_index::File::at(path, gix_hash::Kind::Sha1, false, Default::default()).unwrap(); verify(file) } -pub(crate) fn try_file(name: &str) -> Result { - let file = gix_index::File::at( - crate::fixture_index_path(name), - gix_hash::Kind::Sha1, - false, - Default::default(), - )?; +pub(crate) fn try_file(name: &str, needs_archive: bool) -> Result { + let path = if needs_archive { + crate::fixture_index_path_needs_archive(name) + } else { + crate::fixture_index_path(name) + }; + let file = gix_index::File::at(path, gix_hash::Kind::Sha1, false, Default::default())?; Ok(verify(file)) } pub(crate) fn file(name: &str) -> gix_index::File { - try_file(name).unwrap() + try_file(name, false).unwrap() } /// Needed if we have to freeze the fixture if contents depends on filesystem traversal order /// This is Ok and similar to our manual copies of indices, except that it can be regenerated. fn file_needs_archive(name: &str) -> gix_index::File { - let file = gix_index::File::at( - crate::fixture_index_path_needs_archive(name), - gix_hash::Kind::Sha1, - false, - Default::default(), - ) - .unwrap(); - verify(file) + try_file(name, true).unwrap() } fn file_opt(name: &str, opts: gix_index::decode::Options) -> gix_index::File { let file = gix_index::File::at(crate::fixture_index_path(name), gix_hash::Kind::Sha1, false, opts).unwrap(); verify(file) } -fn with_untracked_snapshot_filters(run: impl FnOnce()) { +fn with_index_file_snapshot_filters(has_stable_mtimes: bool, run: impl FnOnce()) { let mut settings = insta::Settings::clone_current(); - settings.set_filters(vec![ + let stat_filter = if has_stable_mtimes { + ( + r"(?s)Stat \{\s+mtime: Time \{\s+secs: (\d+),\s+nsecs: (\d+),\s+\},\s+ctime: Time \{\s+secs: \d+,\s+nsecs: \d+,\s+\},\s+dev: \d+,\s+ino: \d+,\s+uid: \d+,\s+gid: \d+,\s+size: \d+,\s+\}", + "Stat { mtime: Time { secs: $1, nsecs: $2 }, ctime: Time { ... }, ... }", + ) + } else { + ( + r"(?s)Stat \{\s+mtime: Time \{\s+secs: \d+,\s+nsecs: \d+,\s+\},\s+ctime: Time \{\s+secs: \d+,\s+nsecs: \d+,\s+\},\s+dev: \d+,\s+ino: \d+,\s+uid: \d+,\s+gid: \d+,\s+size: \d+,\s+\}", + "Stat { ... }", + ) + }; + let mut filters = vec![ + (r#"(path: )"[^"]*""#, r#"$1"[redacted]""#), (r#"(identifier: )"[^"]*""#, r#"$1"[redacted]""#), ( - r"(?s)Stat \{\s+mtime: Time \{\s+secs: (\d+),\s+nsecs: (\d+),\s+\},\s+ctime: Time \{\s+secs: (\d+),\s+nsecs: (\d+),\s+\},\s+dev: \d+,\s+ino: \d+,\s+uid: \d+,\s+gid: \d+,\s+size: \d+,\s+\}", - "Stat { mtime: Time { secs: $1, nsecs: $2 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }", + r"(?s)FileTime \{\s+seconds: \d+,\s+nanos: \d+,\s+\}", + "FileTime { ... }", ), - ]); + stat_filter, + ]; + if !has_stable_mtimes { + filters.push((r" mtime: Time \{ secs: \d+, nsecs: \d+ \}", "")); + } + settings.set_filters(filters); settings.bind(run); } @@ -124,28 +134,57 @@ fn v2_empty_skip_hash() { #[test] fn v2_with_multiple_entries_without_eoie_ext() { - let file = file("v2_more_files"); - assert_eq!(file.version(), Version::V2); - - assert_eq!(file.entries().len(), 6); - for (idx, path) in ["a", "b", "c", "d/a", "d/b", "d/c"].iter().enumerate() { - let e = &file.entries()[idx]; - assert_eq!(e.path(&file), path); - assert!(e.flags.is_empty()); - assert_eq!(e.mode, entry::Mode::FILE); - assert_eq!(e.id, hex_to_id("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391")); - } - - let tree = file.tree().unwrap(); - assert_eq!(tree.id, hex_to_id("c9b29c3168d8e677450cc650238b23d9390801fb")); - assert_eq!(tree.num_entries.unwrap_or_default(), 6); - assert!(tree.name.is_empty()); - assert_eq!(tree.children.len(), 1); - - let tree = &tree.children[0]; - assert_eq!(tree.id, hex_to_id("765b32c65d38f04c4f287abda055818ec0f26912")); - assert_eq!(tree.num_entries.unwrap_or_default(), 3); - assert_eq!(tree.name.as_bstr(), "d"); + let file = file_needs_archive("v2_more_files"); + with_index_file_snapshot_filters(true, || { + insta::assert_snapshot!(format!("{file:#?}"), @r#" + File { + path: "[redacted]", + checksum: Some( + Sha1(43bcf12743f506ab5fefaf13f8f5a7eed3d747fe), + ), + object_hash: Sha1, + timestamp: FileTime { ... }, + version: V2, + entries: [ + Mode(FILE) mtime: Time { secs: 1717397605, nsecs: 248416030 } e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a, + Mode(FILE) mtime: Time { secs: 1717397605, nsecs: 248416030 } e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 b, + Mode(FILE) mtime: Time { secs: 1717397605, nsecs: 248416030 } e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 c, + Mode(FILE) mtime: Time { secs: 1717397605, nsecs: 256416095 } e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 d/a, + Mode(FILE) mtime: Time { secs: 1717397605, nsecs: 256416095 } e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 d/b, + Mode(FILE) mtime: Time { secs: 1717397605, nsecs: 256416095 } e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 d/c, + ], + path_backing_size_bytes: 12, + is_sparse: false, + end_of_index_at_decode_time: false, + offset_table_at_decode_time: false, + tree: Some( + Tree { + name: [], + id: Sha1(c9b29c3168d8e677450cc650238b23d9390801fb), + num_entries: Some( + 6, + ), + children: [ + Tree { + name: [ + 100, + ], + id: Sha1(765b32c65d38f04c4f287abda055818ec0f26912), + num_entries: Some( + 3, + ), + children: [], + }, + ], + }, + ), + has_link: false, + has_resolve_undo: false, + untracked: None, + has_fs_monitor: false, + } + "#); + }); } fn find_shared_index_for(index: impl AsRef) -> PathBuf { @@ -226,17 +265,40 @@ fn untr_extension_with_oids() { #[test] fn untr_extension_empty() { let file = file_needs_archive("untracked_cache_empty"); - let untracked = file.untracked().expect("untracked cache extension present"); - - with_untracked_snapshot_filters(|| { - insta::assert_debug_snapshot!(untracked, @r#" - UntrackedCache { - identifier: "[redacted]", - info_exclude: None, - excludes_file: None, - exclude_filename_per_dir: ".gitignore", - dir_flags: 6, - directories: [], + + with_index_file_snapshot_filters(false, || { + insta::assert_debug_snapshot!(&file, @r#" + File { + path: "[redacted]", + checksum: Some( + Sha1(e6e8bff2dab8feaa4cf41fd352248b0fc10acb56), + ), + object_hash: Sha1, + timestamp: FileTime { ... }, + version: V2, + entries: [ + Mode(FILE) e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 tracked-dir/tracked-file, + Mode(FILE) e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 tracked-root-one, + Mode(FILE) e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 tracked-root-two, + ], + path_backing_size_bytes: 56, + is_sparse: false, + end_of_index_at_decode_time: false, + offset_table_at_decode_time: false, + tree: None, + has_link: false, + has_resolve_undo: false, + untracked: Some( + UntrackedCache { + identifier: "[redacted]", + info_exclude: None, + excludes_file: None, + exclude_filename_per_dir: ".gitignore", + dir_flags: 6, + directories: [], + }, + ), + has_fs_monitor: false, } "#); }); @@ -245,75 +307,98 @@ fn untr_extension_empty() { #[test] fn untr_extension_populated() { let file = file_needs_archive("untracked_cache_populated"); - let untracked = file.untracked().expect("untracked cache extension present"); - - with_untracked_snapshot_filters(|| { - insta::assert_debug_snapshot!(untracked, @r#" - UntrackedCache { - identifier: "[redacted]", - info_exclude: Some( - OidStat { - stat: Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, - id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), - }, + + with_index_file_snapshot_filters(true, || { + insta::assert_debug_snapshot!(&file, @r#" + File { + path: "[redacted]", + checksum: Some( + Sha1(dabefe909b6858676ca56f46db0d9a30ad0d2a97), ), - excludes_file: None, - exclude_filename_per_dir: ".gitignore", - dir_flags: 6, - directories: [ - Directory { - name: "", - untracked_entries: [ - "untracked-root-file", - "untracked-dir-3/", - "untracked-dir-2/", - ], - sub_directories: [ - 1, - 2, - 3, - ], - stat: Some( - Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, - ), - exclude_file_oid: None, - check_only: false, - }, - Directory { - name: "tracked-dir", - untracked_entries: [], - sub_directories: [], - stat: Some( - Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, - ), - exclude_file_oid: None, - check_only: false, - }, - Directory { - name: "untracked-dir-2", - untracked_entries: [ - "untracked-file-two", - ], - sub_directories: [], - stat: Some( - Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, + object_hash: Sha1, + timestamp: FileTime { ... }, + version: V2, + entries: [ + Mode(FILE) mtime: Time { secs: 2147483647, nsecs: 123456789 } e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 tracked-dir/tracked-file, + Mode(FILE) mtime: Time { secs: 2147483647, nsecs: 123456789 } e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 tracked-root-one, + Mode(FILE) mtime: Time { secs: 2147483647, nsecs: 123456789 } e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 tracked-root-two, + ], + path_backing_size_bytes: 56, + is_sparse: false, + end_of_index_at_decode_time: false, + offset_table_at_decode_time: false, + tree: None, + has_link: false, + has_resolve_undo: false, + untracked: Some( + UntrackedCache { + identifier: "[redacted]", + info_exclude: Some( + OidStat { + stat: Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { ... }, ... }, + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, ), - exclude_file_oid: None, - check_only: true, - }, - Directory { - name: "untracked-dir-3", - untracked_entries: [ - "untracked-file-three", + excludes_file: None, + exclude_filename_per_dir: ".gitignore", + dir_flags: 6, + directories: [ + Directory { + name: "", + untracked_entries: [ + "untracked-root-file", + "untracked-dir-3/", + "untracked-dir-2/", + ], + sub_directories: [ + 1, + 2, + 3, + ], + stat: Some( + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { ... }, ... }, + ), + exclude_file_oid: None, + check_only: false, + }, + Directory { + name: "tracked-dir", + untracked_entries: [], + sub_directories: [], + stat: Some( + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { ... }, ... }, + ), + exclude_file_oid: None, + check_only: false, + }, + Directory { + name: "untracked-dir-2", + untracked_entries: [ + "untracked-file-two", + ], + sub_directories: [], + stat: Some( + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { ... }, ... }, + ), + exclude_file_oid: None, + check_only: true, + }, + Directory { + name: "untracked-dir-3", + untracked_entries: [ + "untracked-file-three", + ], + sub_directories: [], + stat: Some( + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { ... }, ... }, + ), + exclude_file_oid: None, + check_only: true, + }, ], - sub_directories: [], - stat: Some( - Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, - ), - exclude_file_oid: None, - check_only: true, }, - ], + ), + has_fs_monitor: false, } "#); }); @@ -326,108 +411,132 @@ fn untr_extension_populated() { #[test] fn untr_extension_nested() { let file = file_needs_archive("untracked_cache_nested"); - let untracked = file.untracked().expect("untracked cache extension present"); - - with_untracked_snapshot_filters(|| { - insta::assert_debug_snapshot!(untracked, @r#" - UntrackedCache { - identifier: "[redacted]", - info_exclude: Some( - OidStat { - stat: Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, - id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), - }, + + with_index_file_snapshot_filters(true, || { + insta::assert_debug_snapshot!(&file, @r#" + File { + path: "[redacted]", + checksum: Some( + Sha1(bf50cd966cc718b67d3a326d01aa111f78901c1e), ), - excludes_file: None, - exclude_filename_per_dir: ".gitignore", - dir_flags: 6, - directories: [ - Directory { - name: "", - untracked_entries: [ - "untracked-root-file", - "untracked-dir-3/", - "untracked-dir-2/", - ], - sub_directories: [ - 1, - 4, - 5, - ], - stat: Some( - Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, - ), - exclude_file_oid: None, - check_only: false, - }, - Directory { - name: "tracked-dir-with-ignore", - untracked_entries: [ - "visible-untracked-file", - "nested-untracked-dir/", - ], - sub_directories: [ - 2, - ], - stat: Some( - Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, - ), - exclude_file_oid: Some( - Sha1(55535cdccae965cd0ea191aa22df1145a983b2f9), - ), - check_only: false, - }, - Directory { - name: "nested-untracked-dir", - untracked_entries: [ - "deep-untracked-dir/", - ], - sub_directories: [ - 3, - ], - stat: Some( - Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, - ), - exclude_file_oid: None, - check_only: true, - }, - Directory { - name: "deep-untracked-dir", - untracked_entries: [ - "deep-untracked-file", - ], - sub_directories: [], - stat: Some( - Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, - ), - exclude_file_oid: None, - check_only: true, - }, - Directory { - name: "untracked-dir-2", - untracked_entries: [ - "untracked-file-two", - ], - sub_directories: [], - stat: Some( - Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, + object_hash: Sha1, + timestamp: FileTime { ... }, + version: V2, + entries: [ + Mode(FILE) mtime: Time { secs: 2147483647, nsecs: 123456789 } 55535cdccae965cd0ea191aa22df1145a983b2f9 tracked-dir-with-ignore/.gitignore, + Mode(FILE) mtime: Time { secs: 2147483647, nsecs: 123456789 } e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 tracked-dir-with-ignore/tracked-file, + Mode(FILE) mtime: Time { secs: 2147483647, nsecs: 123456789 } e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 tracked-root-one, + Mode(FILE) mtime: Time { secs: 2147483647, nsecs: 123456789 } e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 tracked-root-two, + ], + path_backing_size_bytes: 102, + is_sparse: false, + end_of_index_at_decode_time: false, + offset_table_at_decode_time: false, + tree: None, + has_link: false, + has_resolve_undo: false, + untracked: Some( + UntrackedCache { + identifier: "[redacted]", + info_exclude: Some( + OidStat { + stat: Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { ... }, ... }, + id: Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391), + }, ), - exclude_file_oid: None, - check_only: true, - }, - Directory { - name: "untracked-dir-3", - untracked_entries: [ - "untracked-file-three", + excludes_file: None, + exclude_filename_per_dir: ".gitignore", + dir_flags: 6, + directories: [ + Directory { + name: "", + untracked_entries: [ + "untracked-root-file", + "untracked-dir-3/", + "untracked-dir-2/", + ], + sub_directories: [ + 1, + 4, + 5, + ], + stat: Some( + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { ... }, ... }, + ), + exclude_file_oid: None, + check_only: false, + }, + Directory { + name: "tracked-dir-with-ignore", + untracked_entries: [ + "visible-untracked-file", + "nested-untracked-dir/", + ], + sub_directories: [ + 2, + ], + stat: Some( + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { ... }, ... }, + ), + exclude_file_oid: Some( + Sha1(55535cdccae965cd0ea191aa22df1145a983b2f9), + ), + check_only: false, + }, + Directory { + name: "nested-untracked-dir", + untracked_entries: [ + "deep-untracked-dir/", + ], + sub_directories: [ + 3, + ], + stat: Some( + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { ... }, ... }, + ), + exclude_file_oid: None, + check_only: true, + }, + Directory { + name: "deep-untracked-dir", + untracked_entries: [ + "deep-untracked-file", + ], + sub_directories: [], + stat: Some( + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { ... }, ... }, + ), + exclude_file_oid: None, + check_only: true, + }, + Directory { + name: "untracked-dir-2", + untracked_entries: [ + "untracked-file-two", + ], + sub_directories: [], + stat: Some( + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { ... }, ... }, + ), + exclude_file_oid: None, + check_only: true, + }, + Directory { + name: "untracked-dir-3", + untracked_entries: [ + "untracked-file-three", + ], + sub_directories: [], + stat: Some( + Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { ... }, ... }, + ), + exclude_file_oid: None, + check_only: true, + }, ], - sub_directories: [], - stat: Some( - Stat { mtime: Time { secs: 2147483647, nsecs: 123456789 }, ctime: Time { secs: [redacted], nsecs: [redacted] }, ... }, - ), - exclude_file_oid: None, - check_only: true, }, - ], + ), + has_fs_monitor: false, } "#); }); @@ -558,7 +667,7 @@ fn v2_split_index() { #[test] fn v2_split_index_recursion_is_handled_gracefully() { - let err = try_file("v2_split_index_recursive").expect_err("recursion fails gracefully"); + let err = try_file("v2_split_index_recursive", false).expect_err("recursion fails gracefully"); assert!(matches!( err, gix_index::file::init::Error::Decode(gix_index::decode::Error::Verify(_)) diff --git a/tests/tools/src/lib.rs b/tests/tools/src/lib.rs index b456f46cc7a..e492b021b04 100644 --- a/tests/tools/src/lib.rs +++ b/tests/tools/src/lib.rs @@ -984,6 +984,7 @@ where &archive_name, object_hash, &script_identity, + None, ); let _marker = marker_if_needed(destination_dir, archive_name)?; @@ -1024,15 +1025,19 @@ fn force_and_dir( archive_name: impl AsRef, object_hash: Option, script_identity: &dyn std::fmt::Display, + cache_variant: Option<&str>, ) -> (bool, PathBuf) { destination_dir.map_or_else( || { - let dir = fixture_base.join( + let mut dir = fixture_base.join( Path::new("generated-do-not-edit") .join(archive_name) - .join(object_hash.unwrap_or_else(self::object_hash).to_string()) - .join(format!("{}-{}", script_identity, family_name())), + .join(object_hash.unwrap_or_else(self::object_hash).to_string()), ); + if let Some(cache_variant) = cache_variant { + dir = dir.join(cache_variant); + } + let dir = dir.join(format!("{}-{}", script_identity, family_name())); (false, dir) }, |d| (true, d.to_owned()), @@ -1207,6 +1212,7 @@ where script_basename, Some(object_hash), &script_identity, + needs_archive.then_some("archive"), ); let _marker = marker_if_needed(destination_dir, script_basename)?; diff --git a/tests/tools/src/tests.rs b/tests/tools/src/tests.rs index 1cd63a03ac1..9634ca7c34c 100644 --- a/tests/tools/src/tests.rs +++ b/tests/tools/src/tests.rs @@ -254,3 +254,28 @@ fn gitignore_fallback_normalizes_windows_path_separators() { Path::new(r"generated-archives\rust-basic.tar") )); } + +#[test] +fn archive_required_fixtures_use_a_separate_cache_directory() { + // Archive-required fixtures must not share the normal generated fixture + // cache. Otherwise, a previous script run can leave platform-specific + // output behind and make a later archive-required request skip extraction. + // Using different paths makes sure they are actually from the archive if they exist. + let fixture_base = Path::new("tests").join("fixtures"); + let (_, generated_dir) = force_and_dir(None, &fixture_base, "scripted", Some(gix_hash::Kind::Sha1), &1234, None); + let (_, archived_dir) = force_and_dir( + None, + &fixture_base, + "scripted", + Some(gix_hash::Kind::Sha1), + &1234, + Some("archive"), + ); + + assert_ne!(generated_dir, archived_dir); + assert!( + archived_dir + .components() + .any(|component| component.as_os_str() == "archive") + ); +}