Skip to content

Commit

Permalink
Merge pull request #1656 from GitoxideLabs/hasconfig
Browse files Browse the repository at this point in the history
hasconfig:remote.*.url
  • Loading branch information
Byron authored Nov 7, 2024
2 parents 1411289 + d51aec9 commit c5955fc
Show file tree
Hide file tree
Showing 45 changed files with 421 additions and 130 deletions.
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
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
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

0 comments on commit c5955fc

Please sign in to comment.