From b8d3d3e38fa8e1289305ac9d8cbf2f3ca5204f60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 07:12:43 +0000 Subject: [PATCH 1/3] feat!: Add support for :(optional) prefix for path values in gix-config-value It's breaking because it adds the `optional` field to the `Path` type. Co-authored-by: Byron <63622+Byron@users.noreply.github.com> --- gix-config-value/src/path.rs | 35 +++++++- gix-config-value/src/types.rs | 24 ++++++ gix-config-value/tests/value/main.rs | 20 +++++ gix-config-value/tests/value/path.rs | 114 +++++++++++++++++++++++++++ 4 files changed, 192 insertions(+), 1 deletion(-) diff --git a/gix-config-value/src/path.rs b/gix-config-value/src/path.rs index 8646a19de79..52865448ad8 100644 --- a/gix-config-value/src/path.rs +++ b/gix-config-value/src/path.rs @@ -4,6 +4,9 @@ use bstr::BStr; use crate::Path; +/// The prefix used to mark a path as optional in Git configuration files. +const OPTIONAL_PREFIX: &[u8] = b":(optional)"; + /// pub mod interpolate { use std::path::PathBuf; @@ -108,11 +111,41 @@ impl AsRef for Path<'_> { impl<'a> From> for Path<'a> { fn from(value: Cow<'a, BStr>) -> Self { - Path { value } + // Check if the value starts with ":(optional)" prefix + if value.starts_with(OPTIONAL_PREFIX) { + // Strip the prefix while preserving the Cow variant for efficiency: + // - Borrowed data remains borrowed (no allocation) + // - Owned data is modified in-place using drain (no extra allocation) + let stripped = match value { + Cow::Borrowed(b) => Cow::Borrowed(&b[OPTIONAL_PREFIX.len()..]), + Cow::Owned(mut b) => { + b.drain(..OPTIONAL_PREFIX.len()); + Cow::Owned(b) + } + }; + Path { + value: stripped, + optional: true, + } + } else { + Path { + value, + optional: false, + } + } } } impl<'a> Path<'a> { + /// Returns `true` if this path was prefixed with `:(optional)`. + /// + /// Optional paths indicate that it's acceptable if the file doesn't exist. + /// This is typically used for configuration like `blame.ignorerevsfile` where + /// the file might not exist in all repositories. + pub fn is_optional(&self) -> bool { + self.optional + } + /// Interpolates this path into a path usable on the file system. /// /// If this path starts with `~/` or `~user/` or `%(prefix)/` diff --git a/gix-config-value/src/types.rs b/gix-config-value/src/types.rs index 232a55f3f92..75e3b8372c0 100644 --- a/gix-config-value/src/types.rs +++ b/gix-config-value/src/types.rs @@ -41,8 +41,32 @@ pub struct Boolean(pub bool); /// Any value that can be interpreted as a path to a resource on disk. /// /// Git represents file paths as byte arrays, modeled here as owned or borrowed byte sequences. +/// +/// ## Optional Paths +/// +/// Paths can be marked as optional by prefixing them with `:(optional)` in the configuration. +/// This indicates that it's acceptable if the file doesn't exist, which is useful for +/// configuration values like `blame.ignorerevsfile` that may only exist in some repositories. +/// +/// ``` +/// use std::borrow::Cow; +/// use gix_config_value::Path; +/// use bstr::ByteSlice; +/// +/// // Regular path - file is expected to exist +/// let path = Path::from(Cow::Borrowed(b"/etc/gitconfig".as_bstr())); +/// assert!(!path.is_optional()); +/// +/// // Optional path - it's okay if the file doesn't exist +/// let path = Path::from(Cow::Borrowed(b":(optional)~/.gitignore".as_bstr())); +/// assert!(path.is_optional()); +/// assert_eq!(path.value.as_ref(), b"~/.gitignore"); // prefix is stripped +/// ``` #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub struct Path<'a> { /// The path string, un-interpolated pub value: std::borrow::Cow<'a, bstr::BStr>, + /// Whether this path was prefixed with `:(optional)`, indicating it's acceptable if the file doesn't exist. + /// Use `is_optional()` method to check this value. + pub(crate) optional: bool, } diff --git a/gix-config-value/tests/value/main.rs b/gix-config-value/tests/value/main.rs index 13a33c9d8f6..9ec79f919a6 100644 --- a/gix-config-value/tests/value/main.rs +++ b/gix-config-value/tests/value/main.rs @@ -15,3 +15,23 @@ mod boolean; mod color; mod integer; mod path; + +/// Ensure that the `:(optional)` prefix is only recognized for Path types, not for other types +mod optional_prefix_only_for_paths { + use std::borrow::Cow; + use gix_config_value::{Boolean, Integer}; + + #[test] + fn optional_prefix_not_recognized_in_boolean() { + // Boolean should fail to parse this because it's not a valid boolean value + let result = Boolean::try_from(Cow::Borrowed(crate::b(":(optional)true"))); + assert!(result.is_err(), "Boolean should not recognize :(optional) prefix"); + } + + #[test] + fn optional_prefix_not_recognized_in_integer() { + // Integer should fail to parse this because it's not a valid integer value + let result = Integer::try_from(Cow::Borrowed(crate::b(":(optional)42"))); + assert!(result.is_err(), "Integer should not recognize :(optional) prefix"); + } +} diff --git a/gix-config-value/tests/value/path.rs b/gix-config-value/tests/value/path.rs index 999c07b06f9..45f9bb969a7 100644 --- a/gix-config-value/tests/value/path.rs +++ b/gix-config-value/tests/value/path.rs @@ -127,3 +127,117 @@ mod interpolate { std::env::current_dir().unwrap().join(name).into() } } + +mod optional_prefix { + use std::borrow::Cow; + + use bstr::ByteSlice; + use crate::{b, cow_str}; + + #[test] + fn path_without_optional_prefix_is_not_optional() { + let path = gix_config_value::Path::from(Cow::Borrowed(b("/some/path"))); + assert!(!path.is_optional(), "path without prefix should not be optional"); + assert_eq!(path.value.as_ref(), b"/some/path"); + } + + #[test] + fn path_with_optional_prefix_is_optional() { + let path = gix_config_value::Path::from(cow_str(":(optional)/some/path")); + assert!(path.is_optional(), "path with :(optional) prefix should be optional"); + assert_eq!(path.value.as_ref(), b"/some/path", "prefix should be stripped"); + } + + #[test] + fn optional_prefix_with_relative_path() { + let path = gix_config_value::Path::from(cow_str(":(optional)relative/path")); + assert!(path.is_optional()); + assert_eq!(path.value.as_ref(), b"relative/path"); + } + + #[test] + fn optional_prefix_with_tilde_expansion() { + let path = gix_config_value::Path::from(cow_str(":(optional)~/config/file")); + assert!(path.is_optional()); + assert_eq!(path.value.as_ref(), b"~/config/file", "tilde should be preserved for interpolation"); + } + + #[test] + fn optional_prefix_with_prefix_substitution() { + let path = gix_config_value::Path::from(cow_str(":(optional)%(prefix)/share/git")); + assert!(path.is_optional()); + assert_eq!(path.value.as_ref(), b"%(prefix)/share/git", "prefix should be preserved for interpolation"); + } + + #[test] + fn optional_prefix_with_windows_path() { + let path = gix_config_value::Path::from(cow_str(r":(optional)C:\Users\file")); + assert!(path.is_optional()); + assert_eq!(path.value.as_ref(), br"C:\Users\file"); + } + + #[test] + fn optional_prefix_followed_by_empty_path() { + let path = gix_config_value::Path::from(cow_str(":(optional)")); + assert!(path.is_optional()); + assert_eq!(path.value.as_ref(), b"", "empty path after prefix is valid"); + } + + #[test] + fn partial_optional_string_is_not_treated_as_prefix() { + let path = gix_config_value::Path::from(cow_str(":(opt)ional/path")); + assert!(!path.is_optional(), "incomplete prefix should not be treated as optional marker"); + assert_eq!(path.value.as_ref(), b":(opt)ional/path"); + } + + #[test] + fn optional_prefix_case_sensitive() { + let path = gix_config_value::Path::from(cow_str(":(OPTIONAL)/some/path")); + assert!(!path.is_optional(), "prefix should be case-sensitive"); + assert_eq!(path.value.as_ref(), b":(OPTIONAL)/some/path"); + } + + #[test] + fn optional_prefix_with_spaces() { + let path = gix_config_value::Path::from(cow_str(":(optional) /path/with/space")); + assert!(path.is_optional()); + assert_eq!(path.value.as_ref(), b" /path/with/space", "space after prefix should be preserved"); + } + + #[test] + fn interpolate_preserves_optional_flag() -> crate::Result { + use gix_config_value::path; + + let path = gix_config_value::Path::from(cow_str(":(optional)/absolute/path")); + assert!(path.is_optional()); + + let interpolated = path.interpolate(path::interpolate::Context::default())?; + assert_eq!(interpolated.as_ref(), std::path::Path::new("/absolute/path")); + + Ok(()) + } + + #[test] + fn borrowed_path_stays_borrowed_after_prefix_stripping() { + // Verify that we don't unnecessarily allocate when stripping the prefix from borrowed data + let borrowed_input: &[u8] = b":(optional)/some/path"; + let path = gix_config_value::Path::from(Cow::Borrowed(borrowed_input.as_bstr())); + + assert!(path.is_optional()); + assert_eq!(path.value.as_ref(), b"/some/path"); + // Verify it's still borrowed (no unnecessary allocation) + assert!(matches!(path.value, Cow::Borrowed(_))); + } + + #[test] + fn owned_path_stays_owned_after_prefix_stripping() { + // Verify that owned data remains owned after prefix stripping (but efficiently) + let owned_input = bstr::BString::from(":(optional)/some/path"); + let path = gix_config_value::Path::from(Cow::Owned(owned_input)); + + assert!(path.is_optional()); + assert_eq!(path.value.as_ref(), b"/some/path"); + // Verify it's still owned + assert!(matches!(path.value, Cow::Owned(_))); + } +} From c2bc9417878f2db6be945b991153c618bf540052 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 4 Dec 2025 14:06:23 +0100 Subject: [PATCH 2/3] refactor - cargo fmt - remove unnecessary tests - keep everything open --- gix-config-value/Cargo.toml | 2 +- gix-config-value/src/path.rs | 24 +++------- gix-config-value/src/types.rs | 13 +++--- gix-config-value/tests/value/main.rs | 20 --------- gix-config-value/tests/value/path.rs | 66 ++++++++++++++-------------- 5 files changed, 50 insertions(+), 75 deletions(-) diff --git a/gix-config-value/Cargo.toml b/gix-config-value/Cargo.toml index 16eae9faae9..aa1cdd2bfd8 100644 --- a/gix-config-value/Cargo.toml +++ b/gix-config-value/Cargo.toml @@ -12,7 +12,7 @@ rust-version = "1.82" include = ["src/**/*", "LICENSE-*"] [lib] -doctest = false +doctest = true [features] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. diff --git a/gix-config-value/src/path.rs b/gix-config-value/src/path.rs index 52865448ad8..64b4c828959 100644 --- a/gix-config-value/src/path.rs +++ b/gix-config-value/src/path.rs @@ -4,9 +4,6 @@ use bstr::BStr; use crate::Path; -/// The prefix used to mark a path as optional in Git configuration files. -const OPTIONAL_PREFIX: &[u8] = b":(optional)"; - /// pub mod interpolate { use std::path::PathBuf; @@ -111,7 +108,9 @@ impl AsRef for Path<'_> { impl<'a> From> for Path<'a> { fn from(value: Cow<'a, BStr>) -> Self { - // Check if the value starts with ":(optional)" prefix + /// The prefix used to mark a path as optional in Git configuration files. + const OPTIONAL_PREFIX: &[u8] = b":(optional)"; + if value.starts_with(OPTIONAL_PREFIX) { // Strip the prefix while preserving the Cow variant for efficiency: // - Borrowed data remains borrowed (no allocation) @@ -125,27 +124,18 @@ impl<'a> From> for Path<'a> { }; Path { value: stripped, - optional: true, + is_optional: true, } } else { Path { value, - optional: false, + is_optional: false, } } } } impl<'a> Path<'a> { - /// Returns `true` if this path was prefixed with `:(optional)`. - /// - /// Optional paths indicate that it's acceptable if the file doesn't exist. - /// This is typically used for configuration like `blame.ignorerevsfile` where - /// the file might not exist in all repositories. - pub fn is_optional(&self) -> bool { - self.optional - } - /// Interpolates this path into a path usable on the file system. /// /// If this path starts with `~/` or `~user/` or `%(prefix)/` @@ -158,8 +148,8 @@ impl<'a> Path<'a> { /// This location is not known at compile time and therefore need to be /// optionally provided by the caller through `git_install_dir`. /// - /// Any other, non-empty path value is returned unchanged and error is returned in case of an empty path value or if required input - /// wasn't provided. + /// Any other, non-empty path value is returned unchanged and error is returned in case of an empty path value or if the required + /// input wasn't provided. pub fn interpolate( self, interpolate::Context { diff --git a/gix-config-value/src/types.rs b/gix-config-value/src/types.rs index 75e3b8372c0..e313960e32a 100644 --- a/gix-config-value/src/types.rs +++ b/gix-config-value/src/types.rs @@ -46,7 +46,7 @@ pub struct Boolean(pub bool); /// /// Paths can be marked as optional by prefixing them with `:(optional)` in the configuration. /// This indicates that it's acceptable if the file doesn't exist, which is useful for -/// configuration values like `blame.ignorerevsfile` that may only exist in some repositories. +/// configuration values like `blame.ignoreRevsFile` that may only exist in some repositories. /// /// ``` /// use std::borrow::Cow; @@ -55,11 +55,11 @@ pub struct Boolean(pub bool); /// /// // Regular path - file is expected to exist /// let path = Path::from(Cow::Borrowed(b"/etc/gitconfig".as_bstr())); -/// assert!(!path.is_optional()); +/// assert!(!path.is_optional); /// /// // Optional path - it's okay if the file doesn't exist /// let path = Path::from(Cow::Borrowed(b":(optional)~/.gitignore".as_bstr())); -/// assert!(path.is_optional()); +/// assert!(path.is_optional); /// assert_eq!(path.value.as_ref(), b"~/.gitignore"); // prefix is stripped /// ``` #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] @@ -67,6 +67,9 @@ pub struct Path<'a> { /// The path string, un-interpolated pub value: std::borrow::Cow<'a, bstr::BStr>, /// Whether this path was prefixed with `:(optional)`, indicating it's acceptable if the file doesn't exist. - /// Use `is_optional()` method to check this value. - pub(crate) optional: bool, + /// + /// Optional paths indicate that it's acceptable if the file doesn't exist. + /// This is typically used for configuration like `blame.ignorerevsfile` where + /// the file might not exist in all repositories. + pub is_optional: bool, } diff --git a/gix-config-value/tests/value/main.rs b/gix-config-value/tests/value/main.rs index 9ec79f919a6..13a33c9d8f6 100644 --- a/gix-config-value/tests/value/main.rs +++ b/gix-config-value/tests/value/main.rs @@ -15,23 +15,3 @@ mod boolean; mod color; mod integer; mod path; - -/// Ensure that the `:(optional)` prefix is only recognized for Path types, not for other types -mod optional_prefix_only_for_paths { - use std::borrow::Cow; - use gix_config_value::{Boolean, Integer}; - - #[test] - fn optional_prefix_not_recognized_in_boolean() { - // Boolean should fail to parse this because it's not a valid boolean value - let result = Boolean::try_from(Cow::Borrowed(crate::b(":(optional)true"))); - assert!(result.is_err(), "Boolean should not recognize :(optional) prefix"); - } - - #[test] - fn optional_prefix_not_recognized_in_integer() { - // Integer should fail to parse this because it's not a valid integer value - let result = Integer::try_from(Cow::Borrowed(crate::b(":(optional)42"))); - assert!(result.is_err(), "Integer should not recognize :(optional) prefix"); - } -} diff --git a/gix-config-value/tests/value/path.rs b/gix-config-value/tests/value/path.rs index 45f9bb969a7..911e1b9db72 100644 --- a/gix-config-value/tests/value/path.rs +++ b/gix-config-value/tests/value/path.rs @@ -131,90 +131,92 @@ mod interpolate { mod optional_prefix { use std::borrow::Cow; - use bstr::ByteSlice; use crate::{b, cow_str}; + use bstr::ByteSlice; #[test] fn path_without_optional_prefix_is_not_optional() { let path = gix_config_value::Path::from(Cow::Borrowed(b("/some/path"))); - assert!(!path.is_optional(), "path without prefix should not be optional"); + assert!(!path.is_optional, "path without prefix should not be optional"); assert_eq!(path.value.as_ref(), b"/some/path"); } #[test] fn path_with_optional_prefix_is_optional() { let path = gix_config_value::Path::from(cow_str(":(optional)/some/path")); - assert!(path.is_optional(), "path with :(optional) prefix should be optional"); + assert!(path.is_optional, "path with :(optional) prefix should be optional"); assert_eq!(path.value.as_ref(), b"/some/path", "prefix should be stripped"); } #[test] fn optional_prefix_with_relative_path() { let path = gix_config_value::Path::from(cow_str(":(optional)relative/path")); - assert!(path.is_optional()); + assert!(path.is_optional); assert_eq!(path.value.as_ref(), b"relative/path"); } #[test] fn optional_prefix_with_tilde_expansion() { let path = gix_config_value::Path::from(cow_str(":(optional)~/config/file")); - assert!(path.is_optional()); - assert_eq!(path.value.as_ref(), b"~/config/file", "tilde should be preserved for interpolation"); + assert!(path.is_optional); + assert_eq!( + path.value.as_ref(), + b"~/config/file", + "tilde should be preserved for interpolation" + ); } #[test] fn optional_prefix_with_prefix_substitution() { let path = gix_config_value::Path::from(cow_str(":(optional)%(prefix)/share/git")); - assert!(path.is_optional()); - assert_eq!(path.value.as_ref(), b"%(prefix)/share/git", "prefix should be preserved for interpolation"); + assert!(path.is_optional); + assert_eq!( + path.value.as_ref(), + b"%(prefix)/share/git", + "prefix should be preserved for interpolation" + ); } #[test] fn optional_prefix_with_windows_path() { let path = gix_config_value::Path::from(cow_str(r":(optional)C:\Users\file")); - assert!(path.is_optional()); + assert!(path.is_optional); assert_eq!(path.value.as_ref(), br"C:\Users\file"); } #[test] fn optional_prefix_followed_by_empty_path() { let path = gix_config_value::Path::from(cow_str(":(optional)")); - assert!(path.is_optional()); + assert!(path.is_optional); assert_eq!(path.value.as_ref(), b"", "empty path after prefix is valid"); } #[test] fn partial_optional_string_is_not_treated_as_prefix() { let path = gix_config_value::Path::from(cow_str(":(opt)ional/path")); - assert!(!path.is_optional(), "incomplete prefix should not be treated as optional marker"); + assert!( + !path.is_optional, + "incomplete prefix should not be treated as optional marker" + ); assert_eq!(path.value.as_ref(), b":(opt)ional/path"); } #[test] fn optional_prefix_case_sensitive() { let path = gix_config_value::Path::from(cow_str(":(OPTIONAL)/some/path")); - assert!(!path.is_optional(), "prefix should be case-sensitive"); + assert!(!path.is_optional, "prefix should be case-sensitive"); assert_eq!(path.value.as_ref(), b":(OPTIONAL)/some/path"); } #[test] fn optional_prefix_with_spaces() { let path = gix_config_value::Path::from(cow_str(":(optional) /path/with/space")); - assert!(path.is_optional()); - assert_eq!(path.value.as_ref(), b" /path/with/space", "space after prefix should be preserved"); - } - - #[test] - fn interpolate_preserves_optional_flag() -> crate::Result { - use gix_config_value::path; - - let path = gix_config_value::Path::from(cow_str(":(optional)/absolute/path")); - assert!(path.is_optional()); - - let interpolated = path.interpolate(path::interpolate::Context::default())?; - assert_eq!(interpolated.as_ref(), std::path::Path::new("/absolute/path")); - - Ok(()) + assert!(path.is_optional); + assert_eq!( + path.value.as_ref(), + b" /path/with/space", + "space after prefix should be preserved" + ); } #[test] @@ -222,8 +224,8 @@ mod optional_prefix { // Verify that we don't unnecessarily allocate when stripping the prefix from borrowed data let borrowed_input: &[u8] = b":(optional)/some/path"; let path = gix_config_value::Path::from(Cow::Borrowed(borrowed_input.as_bstr())); - - assert!(path.is_optional()); + + assert!(path.is_optional); assert_eq!(path.value.as_ref(), b"/some/path"); // Verify it's still borrowed (no unnecessary allocation) assert!(matches!(path.value, Cow::Borrowed(_))); @@ -231,11 +233,11 @@ mod optional_prefix { #[test] fn owned_path_stays_owned_after_prefix_stripping() { - // Verify that owned data remains owned after prefix stripping (but efficiently) + // Verify that owned data remains owned after prefix stripping let owned_input = bstr::BString::from(":(optional)/some/path"); let path = gix_config_value::Path::from(Cow::Owned(owned_input)); - - assert!(path.is_optional()); + + assert!(path.is_optional); assert_eq!(path.value.as_ref(), b"/some/path"); // Verify it's still owned assert!(matches!(path.value, Cow::Owned(_))); From 580bee9f9cb30beaa31ac5a7c164a91f21e2632d Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 4 Dec 2025 14:23:41 +0100 Subject: [PATCH 3/3] feat: respect the `:(optional)` prefix when interpolating paths via `ConfigSnapshot::trusted_path()`. Optional, but non-existing paths are now returned as `None`. --- gix/src/config/cache/access.rs | 14 +++++++++- gix/src/config/snapshot/access.rs | 6 +++++ .../repository/config/config_snapshot/mod.rs | 27 +++++++++++++++++++ gix/tests/gix/repository/excludes.rs | 16 +++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index 16bb2ce182b..42d2e9e9fea 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -544,7 +544,19 @@ pub(crate) fn trusted_file_path<'config>( let install_dir = crate::path::install_dir().ok(); let home = home_dir(environment); let ctx = config::cache::interpolate_context(install_dir.as_deref(), home.as_deref()); - Some(path.interpolate(ctx)) + + let is_optional = path.is_optional; + let res = path.interpolate(ctx); + if is_optional { + if let Ok(path) = &res { + // As opposed to Git, for a lack of the right error variant, we ignore everything that can't + // be stat'ed, instead of just checking if it doesn't exist via error code. + if path.metadata().is_err() { + return None; + } + } + } + Some(res) } pub(crate) fn home_dir(environment: crate::open::permissions::Environment) -> Option { diff --git a/gix/src/config/snapshot/access.rs b/gix/src/config/snapshot/access.rs index 91cbfc84d8b..22ee3dd9f81 100644 --- a/gix/src/config/snapshot/access.rs +++ b/gix/src/config/snapshot/access.rs @@ -54,6 +54,12 @@ impl<'repo> Snapshot<'repo> { /// Return the trusted and fully interpolated path at `key`, or `None` if there is no such value /// or if no value was found in a trusted file. /// An error occurs if the path could not be interpolated to its final value. + /// + /// ### Optional paths + /// + /// The path can be prefixed with `:(optional)` which means it won't be returned if the interpolated + /// path couldn't be accessed. Note also that this is different from Git, which ignores it only if + /// it doesn't exist. pub fn trusted_path( &self, key: impl gix_config::AsKey, diff --git a/gix/tests/gix/repository/config/config_snapshot/mod.rs b/gix/tests/gix/repository/config/config_snapshot/mod.rs index 60ac133b926..822e0b8fa13 100644 --- a/gix/tests/gix/repository/config/config_snapshot/mod.rs +++ b/gix/tests/gix/repository/config/config_snapshot/mod.rs @@ -33,6 +33,33 @@ fn commit_auto_rollback() -> crate::Result { Ok(()) } +mod trusted_path { + use crate::util::named_repo; + + #[test] + fn optional_is_respected() -> crate::Result { + let mut repo: gix::Repository = named_repo("make_basic_repo.sh")?; + repo.config_snapshot_mut().set_raw_value(&"my.path", "does-not-exist")?; + + let actual = repo + .config_snapshot() + .trusted_path("my.path") + .transpose()? + .expect("is set"); + assert_eq!( + actual.as_ref(), + "does-not-exist", + "the path isn't evaluated by default, and may not exist" + ); + + repo.config_snapshot_mut() + .set_raw_value(&"my.path", ":(optional)does-not-exist")?; + let actual = repo.config_snapshot().trusted_path("my.path").transpose()?; + assert_eq!(actual, None, "non-existing paths aren't returned to the caller"); + Ok(()) + } +} + #[test] fn snapshot_mut_commit_and_forget() -> crate::Result { let mut repo: gix::Repository = named_repo("make_basic_repo.sh")?; diff --git a/gix/tests/gix/repository/excludes.rs b/gix/tests/gix/repository/excludes.rs index 60f70beed96..ce0ac39bb4e 100644 --- a/gix/tests/gix/repository/excludes.rs +++ b/gix/tests/gix/repository/excludes.rs @@ -27,3 +27,19 @@ fn empty_core_excludes() -> crate::Result { .expect("empty paths are now just skipped"); Ok(()) } + +#[test] +fn missing_core_excludes_is_ignored() -> crate::Result { + let mut repo = named_subrepo_opts( + "make_basic_repo.sh", + "empty-core-excludes", + gix::open::Options::default().strict_config(true), + )?; + repo.config_snapshot_mut() + .set_value(&gix::config::tree::Core::EXCLUDES_FILE, "definitely-missing")?; + + let index = repo.index_or_empty()?; + repo.excludes(&index, None, Source::WorktreeThenIdMappingIfNotSkipped) + .expect("the call works as missing excludes files are ignored"); + Ok(()) +}