use std::fs;
use std::path::{Path, PathBuf};
use crate::config::{parse_config_parameters, ConfigSet};
use crate::unicode_normalization::probe_filesystem_normalizes_nfd_to_nfc;
fn parse_ceiling_directories_paths() -> Vec<PathBuf> {
let raw = match std::env::var("GIT_CEILING_DIRECTORIES") {
Ok(val) => val,
Err(_) => return Vec::new(),
};
if raw.is_empty() {
return Vec::new();
}
raw.split(':')
.filter(|s| !s.is_empty())
.filter_map(|s| {
let p = PathBuf::from(s);
if !p.is_absolute() {
return None;
}
Some(
p.canonicalize()
.unwrap_or_else(|_| PathBuf::from(s.trim_end_matches('/'))),
)
})
.collect()
}
fn path_for_ceiling_compare(path: &Path) -> String {
path.to_string_lossy().replace('\\', "/")
}
fn offset_1st_component(path: &str) -> usize {
if path.starts_with('/') {
1
} else {
0
}
}
fn longest_ancestor_length(path: &str, ceilings: &[String]) -> Option<usize> {
if path == "/" {
return None;
}
let mut max_len: Option<usize> = None;
for ceil in ceilings {
let mut len = ceil.len();
while len > 0 && ceil.as_bytes().get(len - 1) == Some(&b'/') {
len -= 1;
}
if len == 0 {
continue;
}
if path.len() <= len + 1 {
continue;
}
if !path.starts_with(&ceil[..len]) {
continue;
}
if path.as_bytes().get(len) != Some(&b'/') {
continue;
}
if path.as_bytes().get(len + 1).is_none() {
continue;
}
max_len = Some(max_len.map_or(len, |m| m.max(len)));
}
max_len
}
fn probe_git_dir_at(dir: &Path) -> Option<PathBuf> {
let dot_git = dir.join(".git");
if dot_git.is_dir() {
return Some(dot_git.canonicalize().unwrap_or(dot_git));
}
if dot_git.is_file() {
let content = fs::read_to_string(&dot_git).ok()?;
for line in content.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("gitdir:") {
let p = Path::new(rest.trim());
let resolved = if p.is_absolute() {
p.to_path_buf()
} else {
dir.join(p)
};
return Some(resolved.canonicalize().unwrap_or(resolved));
}
}
}
None
}
pub fn locate_git_dir_from_cwd(cwd: PathBuf) -> Option<PathBuf> {
let start_canon = cwd.canonicalize().unwrap_or(cwd);
let ceilings: Vec<String> = parse_ceiling_directories_paths()
.into_iter()
.map(|p| path_for_ceiling_compare(&p))
.collect();
let mut dir_buf = path_for_ceiling_compare(&start_canon);
let min_offset = offset_1st_component(&dir_buf);
let mut ceil_offset: isize = longest_ancestor_length(&dir_buf, &ceilings)
.map(|n| n as isize)
.unwrap_or(-1);
if ceil_offset < 0 {
ceil_offset = min_offset as isize - 2;
}
loop {
if let Some(gd) = probe_git_dir_at(Path::new(&dir_buf)) {
return Some(gd);
}
let mut offset: isize = dir_buf.len() as isize;
if offset <= min_offset as isize {
break;
}
loop {
offset -= 1;
if offset <= ceil_offset {
break;
}
if dir_buf
.as_bytes()
.get(offset as usize)
.is_some_and(|b| *b == b'/')
{
break;
}
}
if offset <= ceil_offset {
break;
}
let off_u = offset as usize;
let new_len = if off_u > min_offset {
off_u
} else {
min_offset
};
dir_buf.truncate(new_len);
}
None
}
#[must_use]
pub fn read_core_precomposeunicode(git_dir: &Path) -> Option<bool> {
let path = git_dir.join("config");
let Ok(text) = fs::read_to_string(&path) else {
return None;
};
let mut in_core = false;
let mut last: Option<bool> = None;
for line in text.lines() {
let t = line.trim();
if t.starts_with('[') {
in_core = t.eq_ignore_ascii_case("[core]");
continue;
}
if !in_core {
continue;
}
let Some((k, v)) = t.split_once('=') else {
continue;
};
if !k.trim().eq_ignore_ascii_case("precomposeunicode") {
continue;
}
let v = v.trim();
last = Some(matches!(
v.to_ascii_lowercase().as_str(),
"true" | "yes" | "on" | "1"
));
}
last
}
fn precompose_from_git_config_parameters() -> Option<bool> {
let Ok(raw) = std::env::var("GIT_CONFIG_PARAMETERS") else {
return None;
};
let mut last: Option<bool> = None;
for entry in parse_config_parameters(&raw) {
let Some((k, v)) = entry.split_once('=') else {
continue;
};
if !k.trim().eq_ignore_ascii_case("core.precomposeunicode") {
continue;
}
let v = v.trim();
last = Some(matches!(
v.to_ascii_lowercase().as_str(),
"true" | "yes" | "on" | "1"
));
}
last
}
#[must_use]
pub fn effective_core_precomposeunicode(git_dir: Option<&Path>) -> bool {
if let Some(v) = precompose_from_git_config_parameters() {
return v;
}
let Some(gd) = git_dir else {
return false;
};
ConfigSet::load(Some(gd), true)
.ok()
.and_then(|cfg| cfg.get_bool("core.precomposeunicode").and_then(|r| r.ok()))
.unwrap_or(false)
}
#[must_use]
pub fn filesystem_nfd_nfc_aliases(git_dir: &Path) -> bool {
probe_filesystem_normalizes_nfd_to_nfc(git_dir).unwrap_or(false)
}
#[must_use]
pub fn argv_precompose_enabled(git_dir: Option<&Path>) -> bool {
if !effective_core_precomposeunicode(git_dir) {
return false;
}
let Some(gd) = git_dir else {
return false;
};
filesystem_nfd_nfc_aliases(gd)
}
#[must_use]
pub fn pathspec_precompose_enabled() -> bool {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let gd = locate_git_dir_from_cwd(cwd);
effective_core_precomposeunicode(gd.as_deref())
}