Skip to content

Commit c5955fc

Browse files
authored
Merge pull request #1656 from GitoxideLabs/hasconfig
hasconfig:remote.*.url
2 parents 1411289 + d51aec9 commit c5955fc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+421
-130
lines changed

Cargo.lock

-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crate-status.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -747,7 +747,7 @@ See its [README.md](https://github.com/GitoxideLabs/gitoxide/blob/main/gix-lock/
747747
* all config values as per the `gix-config-value` crate
748748
* **includeIf**
749749
* [x] `gitdir`, `gitdir/i`, and `onbranch`
750-
* [ ] `hasconfig`
750+
* [x] `hasconfig:remote.*.url`
751751
* [x] access values and sections by name and sub-section
752752
* [x] edit configuration in memory, non-destructively
753753
* cross-platform newline handling

gix-config/src/file/includes/mod.rs

+58-26
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,16 @@ impl File<'static> {
2323
/// times. It's recommended use is as part of a multi-step bootstrapping which needs fine-grained control,
2424
/// and unless that's given one should prefer one of the other ways of initialization that resolve includes
2525
/// at the right time.
26+
///
27+
/// # Deviation
28+
///
2629
/// - included values are added after the _section_ that included them, not directly after the value. This is
2730
/// a deviation from how git does it, as it technically adds new value right after the include path itself,
2831
/// technically 'splitting' the section. This can only make a difference if the `include` section also has values
2932
/// which later overwrite portions of the included file, which seems unusual as these would be related to `includes`.
3033
/// We can fix this by 'splitting' the include section if needed so the included sections are put into the right place.
34+
/// - `hasconfig:remote.*.url` will not prevent itself to include files with `[remote "name"]\nurl = x` values, but it also
35+
/// won't match them, i.e. one cannot include something that will cause the condition to match or to always be true.
3136
pub fn resolve_includes(&mut self, options: init::Options<'_>) -> Result<(), Error> {
3237
if options.includes.max_depth == 0 {
3338
return Ok(());
@@ -38,10 +43,11 @@ impl File<'static> {
3843
}
3944

4045
pub(crate) fn resolve(config: &mut File<'static>, buf: &mut Vec<u8>, options: init::Options<'_>) -> Result<(), Error> {
41-
resolve_includes_recursive(config, 0, buf, options)
46+
resolve_includes_recursive(None, config, 0, buf, options)
4247
}
4348

4449
fn resolve_includes_recursive(
50+
search_config: Option<&File<'static>>,
4551
target_config: &mut File<'static>,
4652
depth: u8,
4753
buf: &mut Vec<u8>,
@@ -57,30 +63,34 @@ fn resolve_includes_recursive(
5763
};
5864
}
5965

60-
let mut section_ids_and_include_paths = Vec::new();
61-
for (id, section) in target_config
62-
.section_order
63-
.iter()
64-
.map(|id| (*id, &target_config.sections[id]))
65-
{
66+
for id in target_config.section_order.clone().into_iter() {
67+
let section = &target_config.sections[&id];
6668
let header = &section.header;
6769
let header_name = header.name.as_ref();
70+
let mut paths = None;
6871
if header_name == "include" && header.subsection_name.is_none() {
69-
detach_include_paths(&mut section_ids_and_include_paths, section, id);
72+
paths = Some(gather_paths(section, id));
7073
} else if header_name == "includeIf" {
7174
if let Some(condition) = &header.subsection_name {
7275
let target_config_path = section.meta.path.as_deref();
73-
if include_condition_match(condition.as_ref(), target_config_path, options.includes)? {
74-
detach_include_paths(&mut section_ids_and_include_paths, section, id);
76+
if include_condition_match(
77+
condition.as_ref(),
78+
target_config_path,
79+
search_config.unwrap_or(target_config),
80+
options.includes,
81+
)? {
82+
paths = Some(gather_paths(section, id));
7583
}
7684
}
7785
}
86+
if let Some(paths) = paths {
87+
insert_includes_recursively(paths, target_config, depth, options, buf)?;
88+
}
7889
}
79-
80-
append_followed_includes_recursively(section_ids_and_include_paths, target_config, depth, options, buf)
90+
Ok(())
8191
}
8292

83-
fn append_followed_includes_recursively(
93+
fn insert_includes_recursively(
8494
section_ids_and_include_paths: Vec<(SectionId, crate::Path<'_>)>,
8595
target_config: &mut File<'static>,
8696
depth: u8,
@@ -124,30 +134,26 @@ fn append_followed_includes_recursively(
124134
init::Error::Interpolate(err) => Error::Interpolate(err),
125135
init::Error::Includes(_) => unreachable!("BUG: {:?} not possible due to no-follow options", err),
126136
})?;
127-
resolve_includes_recursive(&mut include_config, depth + 1, buf, options)?;
137+
resolve_includes_recursive(Some(target_config), &mut include_config, depth + 1, buf, options)?;
128138

129139
target_config.append_or_insert(include_config, Some(section_id));
130140
}
131141
Ok(())
132142
}
133143

134-
fn detach_include_paths(
135-
include_paths: &mut Vec<(SectionId, crate::Path<'static>)>,
136-
section: &file::Section<'_>,
137-
id: SectionId,
138-
) {
139-
include_paths.extend(
140-
section
141-
.body
142-
.values("path")
143-
.into_iter()
144-
.map(|path| (id, crate::Path::from(Cow::Owned(path.into_owned())))),
145-
);
144+
fn gather_paths(section: &file::Section<'_>, id: SectionId) -> Vec<(SectionId, crate::Path<'static>)> {
145+
section
146+
.body
147+
.values("path")
148+
.into_iter()
149+
.map(|path| (id, crate::Path::from(Cow::Owned(path.into_owned()))))
150+
.collect()
146151
}
147152

148153
fn include_condition_match(
149154
condition: &BStr,
150155
target_config_path: Option<&Path>,
156+
search_config: &File<'static>,
151157
options: Options<'_>,
152158
) -> Result<bool, Error> {
153159
let mut tokens = condition.splitn(2, |b| *b == b':');
@@ -170,6 +176,32 @@ fn include_condition_match(
170176
gix_glob::wildmatch::Mode::IGNORE_CASE,
171177
),
172178
b"onbranch" => Ok(onbranch_matches(condition, options.conditional).is_some()),
179+
b"hasconfig" => {
180+
let mut tokens = condition.splitn(2, |b| *b == b':');
181+
let (key_glob, value_glob) = match (tokens.next(), tokens.next()) {
182+
(Some(a), Some(b)) => (a, b),
183+
_ => return Ok(false),
184+
};
185+
if key_glob.as_bstr() != "remote.*.url" {
186+
return Ok(false);
187+
}
188+
let Some(sections) = search_config.sections_by_name("remote") else {
189+
return Ok(false);
190+
};
191+
for remote in sections {
192+
for url in remote.values("url") {
193+
let glob_matches = gix_glob::wildmatch(
194+
value_glob.as_bstr(),
195+
url.as_ref(),
196+
gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL,
197+
);
198+
if glob_matches {
199+
return Ok(true);
200+
}
201+
}
202+
}
203+
Ok(false)
204+
}
173205
_ => Ok(false),
174206
}
175207
}

gix-config/tests/Cargo.toml

+1-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ publish = false
1414

1515
[[test]]
1616
name = "config"
17-
path = "config.rs"
17+
path = "config/mod.rs"
1818

1919
[[test]]
2020
name = "mem"
@@ -23,7 +23,6 @@ path = "mem.rs"
2323
[dev-dependencies]
2424
gix-config = { path = ".." }
2525
gix-testtools = { path = "../../tests/tools" }
26-
gix = { path = "../../gix", default-features = false }
2726
gix-ref = { path = "../../gix-ref" }
2827
gix-path = { path = "../../gix-path" }
2928
gix-sec = { path = "../../gix-sec" }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use gix_config::file::{includes, init};
2+
use std::path::{Path, PathBuf};
3+
4+
#[test]
5+
fn simple() -> crate::Result {
6+
let (config, root) = config_with_includes("basic")?;
7+
compare_baseline(&config, "user.this", root.join("expected"));
8+
assert_eq!(config.string("user.that"), None);
9+
Ok(())
10+
}
11+
12+
#[test]
13+
fn inclusion_order() -> crate::Result {
14+
let (config, root) = config_with_includes("inclusion-order")?;
15+
for key in ["one", "two", "three"] {
16+
compare_baseline(&config, format!("user.{key}"), root.join(format!("expected.{key}")));
17+
}
18+
Ok(())
19+
}
20+
21+
#[test]
22+
fn globs() -> crate::Result {
23+
let (config, root) = config_with_includes("globs")?;
24+
for key in ["dss", "dse", "dsm", "ssm"] {
25+
compare_baseline(&config, format!("user.{key}"), root.join(format!("expected.{key}")));
26+
}
27+
assert_eq!(config.string("user.no"), None);
28+
Ok(())
29+
}
30+
31+
#[test]
32+
fn cycle_breaker() -> crate::Result {
33+
for name in ["cycle-breaker-direct", "cycle-breaker-indirect"] {
34+
let (_config, _root) = config_with_includes(name)?;
35+
}
36+
37+
Ok(())
38+
}
39+
40+
#[test]
41+
fn no_cycle() -> crate::Result {
42+
let (config, root) = config_with_includes("no-cycle")?;
43+
compare_baseline(&config, "user.name", root.join("expected"));
44+
Ok(())
45+
}
46+
47+
fn compare_baseline(config: &gix_config::File<'static>, key: impl AsRef<str>, expected: impl AsRef<Path>) {
48+
let expected = expected.as_ref();
49+
let key = key.as_ref();
50+
assert_eq!(
51+
config
52+
.string(key)
53+
.unwrap_or_else(|| panic!("key '{key} should be included"))
54+
.as_ref(),
55+
std::fs::read_to_string(expected)
56+
.unwrap_or_else(|err| panic!("Couldn't find '{expected:?}' for reading: {err}"))
57+
.trim(),
58+
"baseline with git should match: '{key}' != {expected:?}"
59+
);
60+
}
61+
62+
fn config_with_includes(name: &str) -> crate::Result<(gix_config::File<'static>, PathBuf)> {
63+
let root = gix_testtools::scripted_fixture_read_only_standalone("hasconfig.sh")?.join(name);
64+
let options = init::Options {
65+
includes: includes::Options::follow(Default::default(), Default::default()),
66+
..Default::default()
67+
};
68+
69+
let config = gix_config::File::from_paths_metadata(
70+
Some(gix_config::file::Metadata::try_from_path(
71+
root.join("config"),
72+
gix_config::Source::Local,
73+
)?),
74+
options,
75+
)?
76+
.expect("non-empty");
77+
Ok((config, root))
78+
}

gix-config/tests/file/init/from_paths/includes/conditional/mod.rs renamed to gix-config/tests/config/file/init/from_paths/includes/conditional/mod.rs

+16-12
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use gix_testtools::tempfile::tempdir;
99
use crate::file::{cow_str, init::from_paths::escape_backslashes};
1010

1111
mod gitdir;
12+
mod hasconfig;
1213
mod onbranch;
1314

1415
#[test]
@@ -137,18 +138,21 @@ fn options_with_git_dir(git_dir: &Path) -> init::Options<'_> {
137138
}
138139
}
139140

140-
fn git_init(path: impl AsRef<std::path::Path>, bare: bool) -> crate::Result<gix::Repository> {
141-
Ok(gix::ThreadSafeRepository::init_opts(
142-
path,
143-
if bare {
144-
gix::create::Kind::Bare
145-
} else {
146-
gix::create::Kind::WithWorktree
147-
},
148-
gix::create::Options::default(),
149-
gix::open::Options::isolated().config_overrides(["user.name=gitoxide", "user.email=gitoxide@localhost"]),
150-
)?
151-
.to_thread_local())
141+
fn git_init(dir: impl AsRef<std::path::Path>, bare: bool) -> crate::Result {
142+
let dir = dir.as_ref();
143+
let mut args = vec!["init"];
144+
if bare {
145+
args.push("--bare");
146+
}
147+
let output = std::process::Command::new(gix_path::env::exe_invocation())
148+
.args(args)
149+
.arg(dir)
150+
.env_remove("GIT_CONFIG_COUNT")
151+
.env_remove("XDG_CONFIG_HOME")
152+
.output()?;
153+
154+
assert!(output.status.success(), "{output:?}, {dir:?}");
155+
Ok(())
152156
}
153157

154158
fn create_symlink(from: impl AsRef<Path>, to: impl AsRef<Path>) {

0 commit comments

Comments
 (0)