use crate::repo_read_config_text::{CONFIG_TEXT_LIMIT, ConfigEntry, parse_bool, parse_config_text};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
const INCLUDE_DEPTH_LIMIT: usize = 10;
const DISCOVERY_KEYS: [&str; 2] = ["core.bare", "core.worktree"];
const CONTENT_KEYS: [&str; 11] = [
"diff.relative",
"core.ignorecase",
"core.precomposeunicode",
"core.sparsecheckout",
"core.splitindex",
"index.sparse",
"diff.orderfile",
"diff.renames",
"diff.ignoresubmodules",
"log.showsignature",
"i18n.logoutputencoding",
];
pub fn read_following(path: &Path) -> Option<Option<Vec<u8>>> {
let metadata = match std::fs::metadata(path) {
Ok(found) => found,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Some(None),
Err(_) => return None,
};
if !metadata.is_file() || metadata.len() > CONFIG_TEXT_LIMIT as u64 {
return None;
}
return std::fs::read(path).ok().map(Some);
}
pub fn include_target(value: &[u8], including: &Path, home: Option<&Path>) -> Option<PathBuf> {
let text: &str = std::str::from_utf8(value).ok()?;
if let Some(rest) = text.strip_prefix("~/") {
return Some(home?.join(rest));
}
if text.starts_with('~') || text.is_empty() {
return None;
}
let path: &Path = Path::new(text);
if path.is_absolute() {
return Some(path.to_path_buf());
}
return Some(including.parent()?.join(path));
}
pub fn is_include_key(key: &str) -> bool {
return key == "include.path" || (key.starts_with("includeif.") && key.ends_with(".path"));
}
pub fn discovery_key(key: &str) -> bool {
return DISCOVERY_KEYS.contains(&key);
}
pub fn content_key(key: &str) -> bool {
return CONTENT_KEYS.contains(&key) || (key.starts_with("submodule.") && key.ends_with(".ignore"));
}
pub fn file_avoids_keys(path: &Path, home: Option<&Path>, depth: usize, declines: &dyn Fn(&str) -> bool) -> Option<bool> {
if depth > INCLUDE_DEPTH_LIMIT {
return None;
}
let Some(text) = read_following(path)? else {
return Some(true);
};
let entries: Vec<ConfigEntry> = parse_config_text(text.as_slice())?;
return entries_avoid_keys(entries.as_slice(), path, home, depth, declines);
}
pub fn entries_avoid_keys(
entries: &[ConfigEntry],
path: &Path,
home: Option<&Path>,
depth: usize,
declines: &dyn Fn(&str) -> bool,
) -> Option<bool> {
for entry in entries {
if declines(entry.key.as_str()) {
return Some(false);
}
if is_include_key(entry.key.as_str()) {
let target: PathBuf = include_target(entry.value.as_deref()?, path, home)?;
if !file_avoids_keys(target.as_path(), home, depth + 1, declines)? {
return Some(false);
}
}
}
return Some(true);
}
pub fn file_avoids_discovery_keys(path: &Path, home: Option<&Path>, depth: usize) -> Option<bool> {
return file_avoids_keys(path, home, depth, &discovery_key);
}
#[derive(Clone, Debug, Default)]
pub struct ScopeEnvironment {
pub home: Option<PathBuf>,
pub xdg: Option<PathBuf>,
pub no_system: bool,
pub system: Option<PathBuf>,
pub system_default: Option<PathBuf>,
pub global: Option<PathBuf>,
pub unresolvable: bool,
}
impl ScopeEnvironment {
pub fn from_variables(variables: &[(OsString, OsString)]) -> ScopeEnvironment {
let find = |name: &str| -> Option<OsString> {
return variables.iter().rev().find(|(candidate, _)| { return candidate == name; }).map(|(_, value)| { return value.clone(); });
};
let nonempty = |name: &str| -> Option<PathBuf> {
return find(name).filter(|value| { return !value.is_empty(); }).map(PathBuf::from);
};
let (no_system, boolean_ok): (bool, bool) = match find("GIT_CONFIG_NOSYSTEM") {
None => (false, true),
Some(value) if value.is_empty() => (false, true),
Some(value) => match parse_bool(Some(value.as_encoded_bytes())) {
Some(truth) => (truth, true),
None => (true, false),
},
};
let mut scope: ScopeEnvironment = ScopeEnvironment {
home: nonempty("HOME"),
xdg: nonempty("XDG_CONFIG_HOME"),
no_system,
system: find("GIT_CONFIG_SYSTEM").map(PathBuf::from),
system_default: None,
global: find("GIT_CONFIG_GLOBAL").map(PathBuf::from),
unresolvable: !boolean_ok,
};
if boolean_ok && !scope.no_system && scope.system.is_none() {
scope.system_default = crate::git_system_config::system_config_for(variables);
scope.unresolvable = scope.system_default.is_none();
}
let relative: bool = [&scope.home, &scope.xdg, &scope.system, &scope.global].iter().any(|path| { return path.as_ref().is_some_and(|found| { return !found.is_absolute(); }); });
scope.unresolvable = scope.unresolvable || relative;
return scope;
}
pub fn labelled_files(&self) -> Vec<(&'static str, PathBuf)> {
let mut labelled: Vec<(&'static str, PathBuf)> = Vec::new();
let mut files: Vec<PathBuf> = self.files();
if !self.no_system && (self.system.is_some() || self.system_default.is_some()) {
labelled.push(("system", files.remove(0)));
}
labelled.extend(files.into_iter().map(|file| { return ("global", file); }));
return labelled;
}
pub fn files(&self) -> Vec<PathBuf> {
let mut files: Vec<PathBuf> = Vec::new();
if !self.no_system {
files.extend(self.system.clone().or_else(|| { return self.system_default.clone(); }));
}
if let Some(global) = &self.global {
files.push(global.clone());
return files;
}
match (&self.xdg, &self.home) {
(Some(directory), _) => files.push(directory.join("git/config")),
(None, Some(base)) => files.push(base.join(".config/git/config")),
(None, None) => {}
}
if let Some(base) = &self.home {
files.push(base.join(".gitconfig"));
}
return files;
}
}
pub fn outer_scopes_avoid_keys(scope: &ScopeEnvironment, declines: &dyn Fn(&str) -> bool) -> Option<bool> {
if scope.unresolvable {
return None;
}
for file in scope.files() {
if !file_avoids_keys(file.as_path(), scope.home.as_deref(), 0, declines)? {
return Some(false);
}
}
return Some(true);
}
#[cfg(test)]
#[path = "repo_read_config_scan_tests.rs"]
mod tests;