Skip to content

Commit 142cdda

Browse files
CopilotByron
andcommitted
Add support for :(optional) prefix for path values in gix-config-value
Co-authored-by: Byron <[email protected]>
1 parent 522324a commit 142cdda

File tree

4 files changed

+136
-1
lines changed

4 files changed

+136
-1
lines changed

gix-config-value/src/path.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,35 @@ impl AsRef<BStr> for Path<'_> {
108108

109109
impl<'a> From<Cow<'a, BStr>> for Path<'a> {
110110
fn from(value: Cow<'a, BStr>) -> Self {
111-
Path { value }
111+
const OPTIONAL_PREFIX: &[u8] = b":(optional)";
112+
113+
// Check if the value starts with ":(optional)" prefix
114+
if value.starts_with(OPTIONAL_PREFIX) {
115+
// Strip the prefix and create a path marked as optional
116+
let stripped = &value[OPTIONAL_PREFIX.len()..];
117+
Path {
118+
value: Cow::Owned(stripped.into()),
119+
is_optional: true,
120+
}
121+
} else {
122+
Path {
123+
value,
124+
is_optional: false,
125+
}
126+
}
112127
}
113128
}
114129

115130
impl<'a> Path<'a> {
131+
/// Returns `true` if this path was prefixed with `:(optional)`.
132+
///
133+
/// Optional paths indicate that it's acceptable if the file doesn't exist.
134+
/// This is typically used for configuration like `blame.ignorerevsfile` where
135+
/// the file might not exist in all repositories.
136+
pub fn is_optional(&self) -> bool {
137+
self.is_optional
138+
}
139+
116140
/// Interpolates this path into a path usable on the file system.
117141
///
118142
/// If this path starts with `~/` or `~user/` or `%(prefix)/`

gix-config-value/src/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,6 @@ pub struct Boolean(pub bool);
4545
pub struct Path<'a> {
4646
/// The path string, un-interpolated
4747
pub value: std::borrow::Cow<'a, bstr::BStr>,
48+
/// Whether this path was prefixed with `:(optional)`, indicating it's acceptable if the file doesn't exist.
49+
pub is_optional: bool,
4850
}

gix-config-value/tests/value/main.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,23 @@ mod boolean;
1515
mod color;
1616
mod integer;
1717
mod path;
18+
19+
/// Ensure that the `:(optional)` prefix is only recognized for Path types, not for other types
20+
mod optional_prefix_only_for_paths {
21+
use std::borrow::Cow;
22+
use gix_config_value::{Boolean, Integer};
23+
24+
#[test]
25+
fn optional_prefix_not_recognized_in_boolean() {
26+
// Boolean should fail to parse this because it's not a valid boolean value
27+
let result = Boolean::try_from(Cow::Borrowed(crate::b(":(optional)true")));
28+
assert!(result.is_err(), "Boolean should not recognize :(optional) prefix");
29+
}
30+
31+
#[test]
32+
fn optional_prefix_not_recognized_in_integer() {
33+
// Integer should fail to parse this because it's not a valid integer value
34+
let result = Integer::try_from(Cow::Borrowed(crate::b(":(optional)42")));
35+
assert!(result.is_err(), "Integer should not recognize :(optional) prefix");
36+
}
37+
}

gix-config-value/tests/value/path.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,92 @@ mod interpolate {
127127
std::env::current_dir().unwrap().join(name).into()
128128
}
129129
}
130+
131+
mod optional_prefix {
132+
use std::borrow::Cow;
133+
134+
use crate::{b, cow_str};
135+
136+
#[test]
137+
fn path_without_optional_prefix_is_not_optional() {
138+
let path = gix_config_value::Path::from(Cow::Borrowed(b("/some/path")));
139+
assert!(!path.is_optional(), "path without prefix should not be optional");
140+
assert_eq!(path.value.as_ref(), b"/some/path");
141+
}
142+
143+
#[test]
144+
fn path_with_optional_prefix_is_optional() {
145+
let path = gix_config_value::Path::from(cow_str(":(optional)/some/path"));
146+
assert!(path.is_optional(), "path with :(optional) prefix should be optional");
147+
assert_eq!(path.value.as_ref(), b"/some/path", "prefix should be stripped");
148+
}
149+
150+
#[test]
151+
fn optional_prefix_with_relative_path() {
152+
let path = gix_config_value::Path::from(cow_str(":(optional)relative/path"));
153+
assert!(path.is_optional());
154+
assert_eq!(path.value.as_ref(), b"relative/path");
155+
}
156+
157+
#[test]
158+
fn optional_prefix_with_tilde_expansion() {
159+
let path = gix_config_value::Path::from(cow_str(":(optional)~/config/file"));
160+
assert!(path.is_optional());
161+
assert_eq!(path.value.as_ref(), b"~/config/file", "tilde should be preserved for interpolation");
162+
}
163+
164+
#[test]
165+
fn optional_prefix_with_prefix_substitution() {
166+
let path = gix_config_value::Path::from(cow_str(":(optional)%(prefix)/share/git"));
167+
assert!(path.is_optional());
168+
assert_eq!(path.value.as_ref(), b"%(prefix)/share/git", "prefix should be preserved for interpolation");
169+
}
170+
171+
#[test]
172+
fn optional_prefix_with_windows_path() {
173+
let path = gix_config_value::Path::from(cow_str(r":(optional)C:\Users\file"));
174+
assert!(path.is_optional());
175+
assert_eq!(path.value.as_ref(), br"C:\Users\file");
176+
}
177+
178+
#[test]
179+
fn optional_prefix_followed_by_empty_path() {
180+
let path = gix_config_value::Path::from(cow_str(":(optional)"));
181+
assert!(path.is_optional());
182+
assert_eq!(path.value.as_ref(), b"", "empty path after prefix is valid");
183+
}
184+
185+
#[test]
186+
fn partial_optional_string_is_not_treated_as_prefix() {
187+
let path = gix_config_value::Path::from(cow_str(":(opt)ional/path"));
188+
assert!(!path.is_optional(), "incomplete prefix should not be treated as optional marker");
189+
assert_eq!(path.value.as_ref(), b":(opt)ional/path");
190+
}
191+
192+
#[test]
193+
fn optional_prefix_case_sensitive() {
194+
let path = gix_config_value::Path::from(cow_str(":(OPTIONAL)/some/path"));
195+
assert!(!path.is_optional(), "prefix should be case-sensitive");
196+
assert_eq!(path.value.as_ref(), b":(OPTIONAL)/some/path");
197+
}
198+
199+
#[test]
200+
fn optional_prefix_with_spaces() {
201+
let path = gix_config_value::Path::from(cow_str(":(optional) /path/with/space"));
202+
assert!(path.is_optional());
203+
assert_eq!(path.value.as_ref(), b" /path/with/space", "space after prefix should be preserved");
204+
}
205+
206+
#[test]
207+
fn interpolate_preserves_optional_flag() -> crate::Result {
208+
use gix_config_value::path;
209+
210+
let path = gix_config_value::Path::from(cow_str(":(optional)/absolute/path"));
211+
assert!(path.is_optional());
212+
213+
let interpolated = path.interpolate(path::interpolate::Context::default())?;
214+
assert_eq!(interpolated.as_ref(), std::path::Path::new("/absolute/path"));
215+
216+
Ok(())
217+
}
218+
}

0 commit comments

Comments
 (0)