use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ConfigScope {
System,
Global,
Local,
Worktree,
Command,
}
impl fmt::Display for ConfigScope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::System => write!(f, "system"),
Self::Global => write!(f, "global"),
Self::Local => write!(f, "local"),
Self::Worktree => write!(f, "worktree"),
Self::Command => write!(f, "command"),
}
}
}
#[derive(Debug, Clone)]
pub struct ConfigEntry {
pub key: String,
pub value: Option<String>,
pub scope: ConfigScope,
pub file: Option<PathBuf>,
pub line: usize,
}
#[derive(Debug, Clone)]
pub struct ConfigFile {
pub path: PathBuf,
pub scope: ConfigScope,
pub entries: Vec<ConfigEntry>,
raw_lines: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ConfigSet {
entries: Vec<ConfigEntry>,
}
pub fn canonical_key(raw: &str) -> Result<String> {
if raw.contains('\n') || raw.contains('\r') {
return Err(Error::ConfigError(format!("invalid key: '{}'" , raw.replace('\n', "\\n"))));
}
let first_dot = raw
.find('.')
.ok_or_else(|| Error::ConfigError(format!("key does not contain a section: '{raw}'")))?;
let last_dot = raw
.rfind('.')
.ok_or_else(|| Error::ConfigError(format!("key does not contain a section: '{raw}'")))?;
if last_dot == raw.len() - 1 {
return Err(Error::ConfigError(format!(
"key does not contain variable name: '{raw}'"
)));
}
let section = &raw[..first_dot];
let name = &raw[last_dot + 1..];
if section.is_empty() || !section.chars().all(|c| c.is_alphanumeric() || c == '-') {
return Err(Error::ConfigError(format!("invalid key (bad section): '{raw}'")));
}
if name.is_empty()
|| !name.chars().next().unwrap().is_ascii_alphabetic()
|| !name.chars().all(|c| c.is_alphanumeric() || c == '-')
{
return Err(Error::ConfigError(format!("invalid key (bad variable name): '{raw}'")));
}
if first_dot == last_dot {
Ok(format!(
"{}.{}",
section.to_lowercase(),
name.to_lowercase()
))
} else {
let subsection = &raw[first_dot + 1..last_dot];
Ok(format!(
"{}.{}.{}",
section.to_lowercase(),
subsection,
name.to_lowercase()
))
}
}
struct Parser {
section: String,
subsection: Option<String>,
}
impl Parser {
fn new() -> Self {
Self {
section: String::new(),
subsection: None,
}
}
fn make_key(&self, name: &str) -> String {
let sec = self.section.to_lowercase();
let var = name.to_lowercase();
match &self.subsection {
Some(sub) => format!("{sec}.{sub}.{var}"),
None => format!("{sec}.{var}"),
}
}
fn try_parse_section(&mut self, line: &str) -> bool {
let trimmed = line.trim();
if !trimmed.starts_with('[') {
return false;
}
let end = match trimmed.find(']') {
Some(i) => i,
None => return false,
};
let inside = &trimmed[1..end];
if let Some(quote_start) = inside.find('"') {
self.section = inside[..quote_start].trim().to_owned();
let rest = &inside[quote_start + 1..];
if let Some(quote_end) = rest.find('"') {
self.subsection = Some(rest[..quote_end].to_owned());
} else {
self.subsection = Some(rest.to_owned());
}
} else {
self.section = inside.trim().to_owned();
self.subsection = None;
}
true
}
fn try_parse_entry(&self, line: &str) -> Option<(String, Option<String>)> {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
return None;
}
if trimmed.starts_with('[') {
return None;
}
if self.section.is_empty() {
return None;
}
if let Some(eq_pos) = trimmed.find('=') {
let raw_name = trimmed[..eq_pos].trim();
let raw_value = trimmed[eq_pos + 1..].trim();
let value = strip_inline_comment(raw_value);
let value = unescape_value(&value);
let key = self.make_key(raw_name);
Some((key, Some(value)))
} else {
let raw_name = strip_inline_comment(trimmed);
let key = self.make_key(raw_name.trim());
Some((key, None))
}
}
}
fn value_line_continues(line: &str) -> bool {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
return false;
}
let value_part = match trimmed.find('=') {
Some(pos) => &trimmed[pos + 1..],
None => return false,
};
let mut in_quote = false;
let mut last_was_backslash = false;
let mut in_comment = false;
for ch in value_part.chars() {
if in_comment {
last_was_backslash = false;
continue;
}
match ch {
'"' if !last_was_backslash => {
in_quote = !in_quote;
last_was_backslash = false;
}
'\\' if !last_was_backslash => {
last_was_backslash = true;
continue;
}
'#' | ';' if !in_quote && !last_was_backslash => {
in_comment = true;
last_was_backslash = false;
}
_ => {
last_was_backslash = false;
}
}
}
last_was_backslash && !in_comment
}
fn strip_inline_comment(s: &str) -> String {
let mut in_quote = false;
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'"' => {
in_quote = !in_quote;
result.push(ch);
}
'\\' if in_quote => {
result.push(ch);
if let Some(&next) = chars.peek() {
result.push(next);
chars.next();
}
}
'#' | ';' if !in_quote => break,
_ => result.push(ch),
}
}
let trimmed = result.trim_end();
trimmed.to_owned()
}
fn unescape_value(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(ch) = chars.next() {
match ch {
'"' => { }
'\\' => match chars.next() {
Some('n') => result.push('\n'),
Some('t') => result.push('\t'),
Some('\\') => result.push('\\'),
Some('"') => result.push('"'),
Some(other) => {
result.push('\\');
result.push(other);
}
None => result.push('\\'),
},
_ => result.push(ch),
}
}
result
}
fn escape_value(s: &str) -> String {
let needs_quoting = s.starts_with(' ')
|| s.starts_with('\t')
|| s.ends_with(' ')
|| s.ends_with('\t')
|| s.contains('"')
|| s.contains('\\')
|| s.contains('\n')
|| s.contains('#')
|| s.contains(';');
if !needs_quoting {
return s.to_owned();
}
let mut out = String::with_capacity(s.len() + 4);
out.push('"');
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
other => out.push(other),
}
}
out.push('"');
out
}
impl ConfigFile {
pub fn parse(path: &Path, content: &str, scope: ConfigScope) -> Result<Self> {
let raw_lines: Vec<String> = content.lines().map(String::from).collect();
let mut entries = Vec::new();
let mut parser = Parser::new();
let mut idx = 0;
while idx < raw_lines.len() {
let start_idx = idx;
let line = &raw_lines[idx];
idx += 1;
let trimmed = line.trim();
if trimmed.starts_with('#') || trimmed.starts_with(';') {
continue;
}
if parser.try_parse_section(line) {
continue;
}
let mut logical_line = line.clone();
while value_line_continues(&logical_line) && idx < raw_lines.len() {
let t = logical_line.trim_end();
logical_line = t[..t.len() - 1].to_string();
let next = raw_lines[idx].trim_start();
logical_line.push_str(next);
idx += 1;
}
if let Some((key, value)) = parser.try_parse_entry(&logical_line) {
entries.push(ConfigEntry {
key,
value,
scope,
file: Some(path.to_path_buf()),
line: start_idx + 1,
});
}
}
Ok(Self {
path: path.to_path_buf(),
scope,
entries,
raw_lines,
})
}
pub fn from_path(path: &Path, scope: ConfigScope) -> Result<Option<Self>> {
match fs::read_to_string(path) {
Ok(content) => Ok(Some(Self::parse(path, &content, scope)?)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(Error::Io(e)),
}
}
pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
let canon = canonical_key(key)?;
let var_lower = raw_variable_name(key).to_lowercase();
let existing_idx = self.entries.iter().rposition(|e| e.key == canon);
if let Some(idx) = existing_idx {
let line_idx = self.entries[idx].line - 1;
self.raw_lines[line_idx] = format!("\t{} = {}", var_lower, escape_value(value));
self.entries[idx].value = Some(value.to_owned());
} else {
let (section, subsection, _var) = split_key(&canon)?;
let (_raw_sec, raw_sub) = raw_section_parts(key);
let section_line = self.find_or_create_section_preserving_case(
§ion, subsection.as_deref(),
§ion, raw_sub.as_deref(),
);
let new_line = format!("\t{} = {}", var_lower, escape_value(value));
let insert_at = self.last_line_in_section(section_line) + 1;
self.raw_lines.insert(insert_at, new_line);
let content = self.raw_lines.join("\n");
let reparsed = Self::parse(&self.path, &content, self.scope)?;
self.entries = reparsed.entries;
self.raw_lines = reparsed.raw_lines;
}
Ok(())
}
pub fn replace_all(&mut self, key: &str, value: &str, value_pattern: Option<&str>) -> Result<()> {
let canon = canonical_key(key)?;
let var_lower = raw_variable_name(key).to_lowercase();
let re = match value_pattern {
Some(pat) => Some(
regex::Regex::new(pat)
.map_err(|e| Error::ConfigError(format!("invalid value-pattern regex: {e}")))?),
None => None,
};
let matching_indices: Vec<usize> = self
.entries
.iter()
.enumerate()
.filter(|(_, e)| {
if e.key != canon {
return false;
}
if let Some(ref re) = re {
let v = e.value.as_deref().unwrap_or("");
re.is_match(v)
} else {
true
}
})
.map(|(i, _)| i)
.collect();
if matching_indices.is_empty() {
return self.set(key, value);
}
let first_match = matching_indices[0];
let lines_to_remove: Vec<usize> = matching_indices
.iter()
.skip(1)
.map(|&i| self.entries[i].line - 1)
.collect();
let first_line_idx = self.entries[first_match].line - 1;
self.raw_lines[first_line_idx] = format!("\t{} = {}", var_lower, escape_value(value));
self.entries[first_match].value = Some(value.to_owned());
for &line_idx in lines_to_remove.iter().rev() {
self.raw_lines.remove(line_idx);
}
let content = self.raw_lines.join("\n");
let reparsed = Self::parse(&self.path, &content, self.scope)?;
self.entries = reparsed.entries;
self.raw_lines = reparsed.raw_lines;
Ok(())
}
pub fn count(&self, key: &str) -> Result<usize> {
let canon = canonical_key(key)?;
Ok(self.entries.iter().filter(|e| e.key == canon).count())
}
pub fn unset_last(&mut self, key: &str) -> Result<usize> {
let canon = canonical_key(key)?;
let last_idx = self.entries.iter().rposition(|e| e.key == canon);
if let Some(idx) = last_idx {
let line_idx = self.entries[idx].line - 1;
self.raw_lines.remove(line_idx);
let content = self.raw_lines.join("\n");
let reparsed = Self::parse(&self.path, &content, self.scope)?;
self.entries = reparsed.entries;
self.raw_lines = reparsed.raw_lines;
Ok(1)
} else {
Ok(0)
}
}
pub fn unset(&mut self, key: &str) -> Result<usize> {
let canon = canonical_key(key)?;
let line_indices: Vec<usize> = self
.entries
.iter()
.filter(|e| e.key == canon)
.map(|e| e.line - 1)
.collect();
let count = line_indices.len();
for &idx in line_indices.iter().rev() {
self.raw_lines.remove(idx);
}
if count > 0 {
let content = self.raw_lines.join("\n");
let reparsed = Self::parse(&self.path, &content, self.scope)?;
self.entries = reparsed.entries;
self.raw_lines = reparsed.raw_lines;
}
Ok(count)
}
pub fn unset_matching(&mut self, key: &str, value_pattern: Option<&str>) -> Result<usize> {
let canon = canonical_key(key)?;
let re = match value_pattern {
Some(pat) => Some(
regex::Regex::new(pat)
.map_err(|e| Error::ConfigError(format!("invalid value-pattern regex: {e}")))?),
None => None,
};
let line_indices: Vec<usize> = self
.entries
.iter()
.filter(|e| {
if e.key != canon {
return false;
}
if let Some(ref re) = re {
let v = e.value.as_deref().unwrap_or("");
re.is_match(v)
} else {
true
}
})
.map(|e| e.line - 1)
.collect();
let count = line_indices.len();
for &idx in line_indices.iter().rev() {
self.raw_lines.remove(idx);
}
if count > 0 {
let content = self.raw_lines.join("\n");
let reparsed = Self::parse(&self.path, &content, self.scope)?;
self.entries = reparsed.entries;
self.raw_lines = reparsed.raw_lines;
}
Ok(count)
}
pub fn remove_section(&mut self, section: &str) -> Result<bool> {
let (sec_name, sub_name) = parse_section_name(section);
let sec_lower = sec_name.to_lowercase();
let mut start = None;
let mut end = 0;
let mut parser = Parser::new();
for (idx, line) in self.raw_lines.iter().enumerate() {
if parser.try_parse_section(line) {
if parser.section.to_lowercase() == sec_lower
&& parser.subsection.as_deref() == sub_name
{
start = Some(idx);
end = idx;
} else if start.is_some() {
break;
}
} else if start.is_some() {
end = idx;
}
}
if let Some(s) = start {
self.raw_lines.drain(s..=end);
let content = self.raw_lines.join("\n");
let reparsed = Self::parse(&self.path, &content, self.scope)?;
self.entries = reparsed.entries;
self.raw_lines = reparsed.raw_lines;
Ok(true)
} else {
Ok(false)
}
}
pub fn rename_section(&mut self, old_name: &str, new_name: &str) -> Result<bool> {
let (old_sec, old_sub) = parse_section_name(old_name);
let (new_sec, new_sub) = parse_section_name(new_name);
let old_lower = old_sec.to_lowercase();
let mut found = false;
let mut parser = Parser::new();
for idx in 0..self.raw_lines.len() {
let line = &self.raw_lines[idx];
if parser.try_parse_section(line)
&& parser.section.to_lowercase() == old_lower
&& parser.subsection.as_deref() == old_sub
{
let header = match new_sub {
Some(sub) => format!("[{} \"{}\"]", new_sec, sub),
None => format!("[{}]", new_sec),
};
self.raw_lines[idx] = header;
found = true;
}
}
if found {
let content = self.raw_lines.join("\n");
let reparsed = Self::parse(&self.path, &content, self.scope)?;
self.entries = reparsed.entries;
self.raw_lines = reparsed.raw_lines;
}
Ok(found)
}
pub fn add_value(&mut self, key: &str, value: &str) -> Result<()> {
let canon = canonical_key(key)?;
let raw_var = raw_variable_name(key);
let (section, subsection, _var) = split_key(&canon)?;
let (raw_sec, raw_sub) = raw_section_parts(key);
let section_line = self.find_or_create_section_preserving_case(
§ion, subsection.as_deref(),
&raw_sec, raw_sub.as_deref(),
);
let new_line = format!("\t{} = {}", raw_var, escape_value(value));
let insert_at = self.last_line_in_section(section_line) + 1;
self.raw_lines.insert(insert_at, new_line);
let content = self.raw_lines.join("\n");
let reparsed = Self::parse(&self.path, &content, self.scope)?;
self.entries = reparsed.entries;
self.raw_lines = reparsed.raw_lines;
Ok(())
}
pub fn write(&self) -> Result<()> {
let content = self.raw_lines.join("\n");
let content = if content.ends_with('\n') {
content
} else {
format!("{content}\n")
};
fs::write(&self.path, content)?;
Ok(())
}
#[allow(dead_code)]
fn find_or_create_section(&mut self, section: &str, subsection: Option<&str>) -> usize {
let sec_lower = section.to_lowercase();
let mut parser = Parser::new();
for (idx, line) in self.raw_lines.iter().enumerate() {
if parser.try_parse_section(line)
&& parser.section.to_lowercase() == sec_lower
&& parser.subsection.as_deref() == subsection
{
return idx;
}
}
let header = match subsection {
Some(sub) => format!("[{} \"{}\"]", section, sub),
None => format!("[{}]", section),
};
self.raw_lines.push(header);
self.raw_lines.len() - 1
}
fn find_or_create_section_preserving_case(
&mut self,
section: &str,
subsection: Option<&str>,
raw_section: &str,
raw_subsection: Option<&str>,
) -> usize {
let sec_lower = section.to_lowercase();
let mut parser = Parser::new();
for (idx, line) in self.raw_lines.iter().enumerate() {
if parser.try_parse_section(line)
&& parser.section.to_lowercase() == sec_lower
&& parser.subsection.as_deref() == subsection
{
return idx;
}
}
let header = match raw_subsection {
Some(sub) => format!("[{} \"{}\"]", raw_section, sub),
None => format!("[{}]", raw_section),
};
self.raw_lines.push(header);
self.raw_lines.len() - 1
}
fn last_line_in_section(&self, section_line: usize) -> usize {
let mut last = section_line;
for idx in (section_line + 1)..self.raw_lines.len() {
let trimmed = self.raw_lines[idx].trim();
if trimmed.starts_with('[') {
break;
}
last = idx;
}
last
}
}
impl ConfigSet {
#[must_use]
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn merge(&mut self, file: &ConfigFile) {
self.entries.extend(file.entries.iter().cloned());
}
pub fn add_command_override(&mut self, key: &str, value: &str) -> Result<()> {
let canon = canonical_key(key)?;
self.entries.push(ConfigEntry {
key: canon,
value: Some(value.to_owned()),
scope: ConfigScope::Command,
file: None,
line: 0,
});
Ok(())
}
#[must_use]
pub fn get(&self, key: &str) -> Option<String> {
let canon = canonical_key(key).ok()?;
self.entries
.iter()
.rev()
.find(|e| e.key == canon)
.map(|e| e.value.clone().unwrap_or_else(|| "true".to_owned()))
}
#[must_use]
pub fn get_all(&self, key: &str) -> Vec<String> {
let canon = match canonical_key(key) {
Ok(c) => c,
Err(_) => return Vec::new(),
};
self.entries
.iter()
.filter(|e| e.key == canon)
.map(|e| e.value.clone().unwrap_or_else(|| "true".to_owned()))
.collect()
}
pub fn get_bool(&self, key: &str) -> Option<std::result::Result<bool, String>> {
self.get(key).map(|v| parse_bool(&v))
}
pub fn get_i64(&self, key: &str) -> Option<std::result::Result<i64, String>> {
self.get(key).map(|v| parse_i64(&v))
}
pub fn get_regexp(&self, pattern: &str) -> std::result::Result<Vec<&ConfigEntry>, String> {
let re = regex::Regex::new(pattern)
.map_err(|e| format!("invalid key pattern: {e}"))?;
Ok(self.entries
.iter()
.filter(|e| re.is_match(&e.key))
.collect())
}
#[must_use]
pub fn entries(&self) -> &[ConfigEntry] {
&self.entries
}
pub fn load(git_dir: Option<&Path>, include_system: bool) -> Result<Self> {
let mut set = Self::new();
if include_system {
if let Ok(Some(f)) =
ConfigFile::from_path(Path::new("/etc/gitconfig"), ConfigScope::System)
{
Self::merge_with_includes(&mut set, &f, true)?;
}
}
for path in global_config_paths() {
if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
Self::merge_with_includes(&mut set, &f, true)?;
break; }
}
if let Some(gd) = git_dir {
let local_path = gd.join("config");
if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
Self::merge_with_includes(&mut set, &f, true)?;
}
let wt_path = gd.join("config.worktree");
if let Ok(Some(f)) = ConfigFile::from_path(&wt_path, ConfigScope::Worktree) {
Self::merge_with_includes(&mut set, &f, true)?;
}
}
if let Ok(path) = std::env::var("GIT_CONFIG") {
if let Ok(Some(f)) = ConfigFile::from_path(Path::new(&path), ConfigScope::Command) {
set.merge(&f);
}
}
if let Ok(count_str) = std::env::var("GIT_CONFIG_COUNT") {
if let Ok(count) = count_str.parse::<usize>() {
for i in 0..count {
let key_var = format!("GIT_CONFIG_KEY_{i}");
let val_var = format!("GIT_CONFIG_VALUE_{i}");
if let (Ok(key), Ok(val)) = (std::env::var(&key_var), std::env::var(&val_var)) {
let _ = set.add_command_override(&key, &val);
}
}
}
}
if let Ok(params) = std::env::var("GIT_CONFIG_PARAMETERS") {
for entry in parse_config_parameters(¶ms) {
if let Some((key, val)) = entry.split_once('=') {
let _ = set.add_command_override(key.trim(), val.trim());
} else {
let _ = set.add_command_override(entry.trim(), "true");
}
}
}
Ok(set)
}
fn merge_with_includes(
set: &mut Self,
file: &ConfigFile,
process_includes: bool,
) -> Result<()> {
let mut includes: Vec<(String, Option<String>)> = Vec::new();
for entry in &file.entries {
if entry.key == "include.path" {
if let Some(ref val) = entry.value {
includes.push((val.clone(), None));
}
} else if entry.key.starts_with("includeif.") && entry.key.ends_with(".path") {
let mid = &entry.key["includeif.".len()..entry.key.len() - ".path".len()];
if let Some(ref val) = entry.value {
includes.push((val.clone(), Some(mid.to_owned())));
}
}
}
set.merge(file);
if process_includes {
for (inc_path, condition) in includes {
if let Some(ref cond) = condition {
if !evaluate_include_condition(cond, file) {
continue;
}
}
let resolved = resolve_include_path(&inc_path, file.path.parent());
if let Ok(Some(inc_file)) = ConfigFile::from_path(&resolved, file.scope) {
Self::merge_with_includes(set, &inc_file, true)?;
}
}
}
Ok(())
}
}
pub fn parse_bool(s: &str) -> std::result::Result<bool, String> {
match s.to_lowercase().as_str() {
"true" | "yes" | "on" | "" => Ok(true),
"false" | "no" | "off" => Ok(false),
_ => {
if let Ok(n) = s.parse::<i64>() {
return Ok(n != 0);
}
Err(format!("bad boolean config value '{s}'"))
}
}
}
pub fn parse_i64(s: &str) -> std::result::Result<i64, String> {
let s = s.trim();
if s.is_empty() {
return Err("empty integer value".to_owned());
}
let (num_str, multiplier) = match s.as_bytes().last() {
Some(b'k' | b'K') => (&s[..s.len() - 1], 1024_i64),
Some(b'm' | b'M') => (&s[..s.len() - 1], 1024 * 1024),
Some(b'g' | b'G') => (&s[..s.len() - 1], 1024 * 1024 * 1024),
_ => (s, 1_i64),
};
let base: i64 = num_str
.parse()
.map_err(|_| format!("invalid integer: '{s}'"))?;
base.checked_mul(multiplier)
.ok_or_else(|| format!("integer overflow: '{s}'"))
}
pub fn parse_path(s: &str) -> String {
if let Some(rest) = s.strip_prefix("~/") {
if let Some(home) = home_dir() {
return home.join(rest).to_string_lossy().to_string();
}
}
s.to_owned()
}
fn parse_config_parameters(raw: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut iter = raw.chars().peekable();
while let Some(&c) = iter.peek() {
if c == '\'' {
iter.next();
let mut s = String::new();
loop {
match iter.next() {
Some('\'') | None => break,
Some(x) => s.push(x),
}
}
if !s.is_empty() {
out.push(s);
}
} else {
iter.next();
}
}
out
}
fn global_config_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Ok(p) = std::env::var("GIT_CONFIG_GLOBAL") {
paths.push(PathBuf::from(p));
return paths;
}
if let Some(home) = home_dir() {
paths.push(home.join(".gitconfig"));
}
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
paths.push(PathBuf::from(xdg).join("git/config"));
} else if let Some(home) = home_dir() {
paths.push(home.join(".config/git/config"));
}
paths
}
fn home_dir() -> Option<PathBuf> {
std::env::var("HOME").ok().map(PathBuf::from)
}
fn resolve_include_path(path: &str, base: Option<&Path>) -> PathBuf {
let expanded = parse_path(path);
let p = Path::new(&expanded);
if p.is_absolute() {
p.to_path_buf()
} else if let Some(base) = base {
base.join(p)
} else {
p.to_path_buf()
}
}
fn evaluate_include_condition(condition: &str, _file: &ConfigFile) -> bool {
let _ = condition;
false
}
fn split_key(key: &str) -> Result<(String, Option<String>, String)> {
let first_dot = key
.find('.')
.ok_or_else(|| Error::ConfigError(format!("invalid key: '{key}'")))?;
let last_dot = key
.rfind('.')
.ok_or_else(|| Error::ConfigError(format!("invalid key: '{key}'")))?;
let section = key[..first_dot].to_owned();
let variable = key[last_dot + 1..].to_owned();
let subsection = if first_dot == last_dot {
None
} else {
Some(key[first_dot + 1..last_dot].to_owned())
};
Ok((section, subsection, variable))
}
#[allow(dead_code)]
fn variable_name_from_key(key: &str) -> &str {
match key.rfind('.') {
Some(i) => &key[i + 1..],
None => key,
}
}
fn parse_section_name(name: &str) -> (&str, Option<&str>) {
match name.find('.') {
Some(i) => (&name[..i], Some(&name[i + 1..])),
None => (name, None),
}
}
fn raw_variable_name(raw_key: &str) -> &str {
match raw_key.rfind('.') {
Some(i) => &raw_key[i + 1..],
None => raw_key,
}
}
fn raw_section_parts(raw_key: &str) -> (String, Option<String>) {
let first_dot = match raw_key.find('.') {
Some(i) => i,
None => return (raw_key.to_owned(), None),
};
let last_dot = match raw_key.rfind('.') {
Some(i) => i,
None => return (raw_key[..first_dot].to_owned(), None),
};
let section = raw_key[..first_dot].to_owned();
if first_dot == last_dot {
(section, None)
} else {
let subsection = raw_key[first_dot + 1..last_dot].to_owned();
(section, Some(subsection))
}
}