use super::repo_read_config_scan::{ScopeEnvironment, include_target, is_include_key, read_following};
use super::repo_read_config_text::{ConfigEntry, last_entry, parse_bool, parse_config_text};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
const INCLUDE_DEPTH_LIMIT: usize = 10;
const ENV_ENTRY_LIMIT: u64 = 1024;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ListedEntry {
pub scope: &'static str,
pub entry: ConfigEntry,
}
fn add_file(path: &Path, scope: &'static str, home: Option<&Path>, depth: usize, listed: &mut Vec<ListedEntry>) -> Option<()> {
if depth > INCLUDE_DEPTH_LIMIT {
return None;
}
let Some(text) = read_following(path)? else {
return Some(());
};
for entry in parse_config_text(text.as_slice())? {
let include: Option<std::path::PathBuf> = if is_include_key(entry.key.as_str()) {
if entry.key != "include.path" {
return None;
}
Some(include_target(entry.value.as_deref()?, path, home)?)
} else {
None
};
listed.push(ListedEntry { scope, entry });
if let Some(target) = include {
add_file(target.as_path(), scope, home, depth + 1, listed)?;
}
}
return Some(());
}
fn canonical_key(key: &str) -> Option<String> {
let (section, rest) = key.split_once('.')?;
let (middle, name) = match rest.rsplit_once('.') {
Some((subsection, name)) => (Some(subsection), name),
None => (None, rest),
};
let plain = |text: &str| -> bool { return !text.is_empty() && text.bytes().all(|byte| { return byte.is_ascii_alphanumeric() || byte == b'-'; }); };
if !plain(section) || !plain(name) || !name.as_bytes()[0].is_ascii_alphabetic() || middle.is_some_and(|text| { return text.contains('\n'); }) {
return None;
}
return Some(match middle {
Some(subsection) => format!("{}.{subsection}.{}", section.to_ascii_lowercase(), name.to_ascii_lowercase()),
None => format!("{}.{}", section.to_ascii_lowercase(), name.to_ascii_lowercase()),
});
}
fn add_environment(variables: &[(OsString, OsString)], listed: &mut Vec<ListedEntry>) -> Option<()> {
let find = |name: &str| -> Option<OsString> {
return variables.iter().rev().find(|(candidate, _)| { return candidate == name; }).map(|(_, value)| { return value.clone(); });
};
if find("GIT_CONFIG_PARAMETERS").is_some() {
return None;
}
let Some(count_text) = find("GIT_CONFIG_COUNT") else {
return Some(());
};
let count: u64 = super::child_environment::parse_config_count(count_text.as_os_str())?;
if count > ENV_ENTRY_LIMIT {
return None;
}
for index in 0..count {
let key: OsString = find(format!("GIT_CONFIG_KEY_{index}").as_str())?;
let value: OsString = find(format!("GIT_CONFIG_VALUE_{index}").as_str())?;
let entry: ConfigEntry = ConfigEntry { key: canonical_key(key.to_str()?)?, value: Some(value.as_encoded_bytes().to_vec()) };
listed.push(ListedEntry { scope: "command", entry });
}
return Some(());
}
pub fn list_config(common_dir: &Path, git_dir: &Path, scope: &ScopeEnvironment, variables: &[(OsString, OsString)]) -> Option<Vec<ListedEntry>> {
if scope.unresolvable {
return None;
}
let mut listed: Vec<ListedEntry> = Vec::new();
for (label, file) in scope.labelled_files() {
add_file(file.as_path(), label, scope.home.as_deref(), 0, &mut listed)?;
}
let start: usize = listed.len();
let common_file: PathBuf = common_dir.join("config");
add_file(common_file.as_path(), "local", scope.home.as_deref(), 0, &mut listed)?;
let local: Vec<ConfigEntry> = listed[start..].iter().map(|item| { return item.entry.clone(); }).collect();
let direct_text: Vec<u8> = read_following(common_file.as_path())??;
let direct: Vec<ConfigEntry> = parse_config_text(direct_text.as_slice())?;
let listed_switch: Option<&ConfigEntry> = last_entry(local.as_slice(), "extensions.worktreeconfig");
let direct_switch: Option<&ConfigEntry> = last_entry(direct.as_slice(), "extensions.worktreeconfig");
if listed_switch != direct_switch {
return None;
}
let versioned: bool = last_entry(direct.as_slice(), "core.repositoryformatversion").is_some();
if let Some(switch) = direct_switch
&& parse_bool(switch.value.as_deref())?
&& versioned
{
add_file(git_dir.join("config.worktree").as_path(), "worktree", scope.home.as_deref(), 0, &mut listed)?;
}
add_environment(variables, &mut listed)?;
return Some(listed);
}
pub fn listing_bytes(listed: &[ListedEntry]) -> Vec<u8> {
let mut out: Vec<u8> = Vec::new();
for item in listed {
out.extend_from_slice(item.scope.as_bytes());
out.push(0);
out.extend_from_slice(item.entry.key.as_bytes());
if let Some(value) = &item.entry.value {
out.push(b'\n');
out.extend_from_slice(value.as_slice());
}
out.push(0);
}
return out;
}
#[cfg(test)]
#[path = "repo_read_config_list_tests.rs"]
mod tests;