Skip to content

hasconfig:remote.*.url #1656

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crate-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,7 @@ See its [README.md](https://github.com/GitoxideLabs/gitoxide/blob/main/gix-lock/
* all config values as per the `gix-config-value` crate
* **includeIf**
* [x] `gitdir`, `gitdir/i`, and `onbranch`
* [ ] `hasconfig`
* [x] `hasconfig:remote.*.url`
* [x] access values and sections by name and sub-section
* [x] edit configuration in memory, non-destructively
* cross-platform newline handling
Expand Down
84 changes: 58 additions & 26 deletions gix-config/src/file/includes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,16 @@ impl File<'static> {
/// times. It's recommended use is as part of a multi-step bootstrapping which needs fine-grained control,
/// and unless that's given one should prefer one of the other ways of initialization that resolve includes
/// at the right time.
///
/// # Deviation
///
/// - included values are added after the _section_ that included them, not directly after the value. This is
/// a deviation from how git does it, as it technically adds new value right after the include path itself,
/// technically 'splitting' the section. This can only make a difference if the `include` section also has values
/// which later overwrite portions of the included file, which seems unusual as these would be related to `includes`.
/// We can fix this by 'splitting' the include section if needed so the included sections are put into the right place.
/// - `hasconfig:remote.*.url` will not prevent itself to include files with `[remote "name"]\nurl = x` values, but it also
/// won't match them, i.e. one cannot include something that will cause the condition to match or to always be true.
pub fn resolve_includes(&mut self, options: init::Options<'_>) -> Result<(), Error> {
if options.includes.max_depth == 0 {
return Ok(());
Expand All @@ -38,10 +43,11 @@ impl File<'static> {
}

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

fn resolve_includes_recursive(
search_config: Option<&File<'static>>,
target_config: &mut File<'static>,
depth: u8,
buf: &mut Vec<u8>,
Expand All @@ -57,30 +63,34 @@ fn resolve_includes_recursive(
};
}

let mut section_ids_and_include_paths = Vec::new();
for (id, section) in target_config
.section_order
.iter()
.map(|id| (*id, &target_config.sections[id]))
{
for id in target_config.section_order.clone().into_iter() {
let section = &target_config.sections[&id];
let header = &section.header;
let header_name = header.name.as_ref();
let mut paths = None;
if header_name == "include" && header.subsection_name.is_none() {
detach_include_paths(&mut section_ids_and_include_paths, section, id);
paths = Some(gather_paths(section, id));
} else if header_name == "includeIf" {
if let Some(condition) = &header.subsection_name {
let target_config_path = section.meta.path.as_deref();
if include_condition_match(condition.as_ref(), target_config_path, options.includes)? {
detach_include_paths(&mut section_ids_and_include_paths, section, id);
if include_condition_match(
condition.as_ref(),
target_config_path,
search_config.unwrap_or(target_config),
options.includes,
)? {
paths = Some(gather_paths(section, id));
}
}
}
if let Some(paths) = paths {
insert_includes_recursively(paths, target_config, depth, options, buf)?;
}
}

append_followed_includes_recursively(section_ids_and_include_paths, target_config, depth, options, buf)
Ok(())
}

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

target_config.append_or_insert(include_config, Some(section_id));
}
Ok(())
}

fn detach_include_paths(
include_paths: &mut Vec<(SectionId, crate::Path<'static>)>,
section: &file::Section<'_>,
id: SectionId,
) {
include_paths.extend(
section
.body
.values("path")
.into_iter()
.map(|path| (id, crate::Path::from(Cow::Owned(path.into_owned())))),
);
fn gather_paths(section: &file::Section<'_>, id: SectionId) -> Vec<(SectionId, crate::Path<'static>)> {
section
.body
.values("path")
.into_iter()
.map(|path| (id, crate::Path::from(Cow::Owned(path.into_owned()))))
.collect()
}

fn include_condition_match(
condition: &BStr,
target_config_path: Option<&Path>,
search_config: &File<'static>,
options: Options<'_>,
) -> Result<bool, Error> {
let mut tokens = condition.splitn(2, |b| *b == b':');
Expand All @@ -170,6 +176,32 @@ fn include_condition_match(
gix_glob::wildmatch::Mode::IGNORE_CASE,
),
b"onbranch" => Ok(onbranch_matches(condition, options.conditional).is_some()),
b"hasconfig" => {
let mut tokens = condition.splitn(2, |b| *b == b':');
let (key_glob, value_glob) = match (tokens.next(), tokens.next()) {
(Some(a), Some(b)) => (a, b),
_ => return Ok(false),
};
if key_glob.as_bstr() != "remote.*.url" {
return Ok(false);
}
let Some(sections) = search_config.sections_by_name("remote") else {
return Ok(false);
};
for remote in sections {
for url in remote.values("url") {
let glob_matches = gix_glob::wildmatch(
value_glob.as_bstr(),
url.as_ref(),
gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL,
);
if glob_matches {
return Ok(true);
}
}
}
Ok(false)
}
_ => Ok(false),
}
}
Expand Down
3 changes: 1 addition & 2 deletions gix-config/tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ publish = false

[[test]]
name = "config"
path = "config.rs"
path = "config/mod.rs"

[[test]]
name = "mem"
Expand All @@ -23,7 +23,6 @@ path = "mem.rs"
[dev-dependencies]
gix-config = { path = ".." }
gix-testtools = { path = "../../tests/tools" }
gix = { path = "../../gix", default-features = false }
gix-ref = { path = "../../gix-ref" }
gix-path = { path = "../../gix-path" }
gix-sec = { path = "../../gix-sec" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use gix_config::file::{includes, init};
use std::path::{Path, PathBuf};

#[test]
fn simple() -> crate::Result {
let (config, root) = config_with_includes("basic")?;
compare_baseline(&config, "user.this", root.join("expected"));
assert_eq!(config.string("user.that"), None);
Ok(())
}

#[test]
fn inclusion_order() -> crate::Result {
let (config, root) = config_with_includes("inclusion-order")?;
for key in ["one", "two", "three"] {
compare_baseline(&config, format!("user.{key}"), root.join(format!("expected.{key}")));
}
Ok(())
}

#[test]
fn globs() -> crate::Result {
let (config, root) = config_with_includes("globs")?;
for key in ["dss", "dse", "dsm", "ssm"] {
compare_baseline(&config, format!("user.{key}"), root.join(format!("expected.{key}")));
}
assert_eq!(config.string("user.no"), None);
Ok(())
}

#[test]
fn cycle_breaker() -> crate::Result {
for name in ["cycle-breaker-direct", "cycle-breaker-indirect"] {
let (_config, _root) = config_with_includes(name)?;
}

Ok(())
}

#[test]
fn no_cycle() -> crate::Result {
let (config, root) = config_with_includes("no-cycle")?;
compare_baseline(&config, "user.name", root.join("expected"));
Ok(())
}

fn compare_baseline(config: &gix_config::File<'static>, key: impl AsRef<str>, expected: impl AsRef<Path>) {
let expected = expected.as_ref();
let key = key.as_ref();
assert_eq!(
config
.string(key)
.unwrap_or_else(|| panic!("key '{key} should be included"))
.as_ref(),
std::fs::read_to_string(expected)
.unwrap_or_else(|err| panic!("Couldn't find '{expected:?}' for reading: {err}"))
.trim(),
"baseline with git should match: '{key}' != {expected:?}"
);
}

fn config_with_includes(name: &str) -> crate::Result<(gix_config::File<'static>, PathBuf)> {
let root = gix_testtools::scripted_fixture_read_only_standalone("hasconfig.sh")?.join(name);
let options = init::Options {
includes: includes::Options::follow(Default::default(), Default::default()),
..Default::default()
};

let config = gix_config::File::from_paths_metadata(
Some(gix_config::file::Metadata::try_from_path(
root.join("config"),
gix_config::Source::Local,
)?),
options,
)?
.expect("non-empty");
Ok((config, root))
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use gix_testtools::tempfile::tempdir;
use crate::file::{cow_str, init::from_paths::escape_backslashes};

mod gitdir;
mod hasconfig;
mod onbranch;

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

fn git_init(path: impl AsRef<std::path::Path>, bare: bool) -> crate::Result<gix::Repository> {
Ok(gix::ThreadSafeRepository::init_opts(
path,
if bare {
gix::create::Kind::Bare
} else {
gix::create::Kind::WithWorktree
},
gix::create::Options::default(),
gix::open::Options::isolated().config_overrides(["user.name=gitoxide", "user.email=gitoxide@localhost"]),
)?
.to_thread_local())
fn git_init(dir: impl AsRef<std::path::Path>, bare: bool) -> crate::Result {
let dir = dir.as_ref();
let mut args = vec!["init"];
if bare {
args.push("--bare");
}
let output = std::process::Command::new(gix_path::env::exe_invocation())
.args(args)
.arg(dir)
.env_remove("GIT_CONFIG_COUNT")
.env_remove("XDG_CONFIG_HOME")
.output()?;

assert!(output.status.success(), "{output:?}, {dir:?}");
Ok(())
}

fn create_symlink(from: impl AsRef<Path>, to: impl AsRef<Path>) {
Expand Down
Loading
Loading