use std::path::Path;
const GIT_INTERNAL_PREFIXES: &[&str] = &["head", "objects", "refs", "hooks"];
fn normalize_git_path_arg(arg: &str) -> String {
let mut s = arg.to_string();
if !s.is_empty() {
let first_char = s.chars().next().unwrap();
if first_char == '/' || "–—―".contains(first_char) {
if let Some(c) = s[1..].find(':') {
s = s[c + 1..].to_string();
}
}
}
s = s.trim_matches('"').trim_matches('\'').to_string();
s = s.replace('`', "");
let provider_prefixes = ["FileSystem::", "Microsoft.PowerShell.Core\\FileSystem::"];
for prefix in provider_prefixes {
if s.to_lowercase().starts_with(&prefix.to_lowercase()) {
if let Some(pos) = s.find(prefix) {
s = s[pos + prefix.len()..].to_string();
}
}
}
if s.len() >= 2 && s.chars().nth(1) == Some(':') {
if !s
.chars()
.nth(2)
.map(|c| c == '/' || c == '\\')
.unwrap_or(false)
{
s = s[2..].to_string();
}
}
s = s.replace('\\', "/");
let parts: Vec<String> = s
.split('/')
.map(|c| {
if c.is_empty() || c == "." || c == ".." {
c.to_string()
} else {
let mut c = c.trim_end().to_string();
while c.ends_with('.') && c != "." && c != ".." {
c.pop();
}
if c.is_empty() { ".".to_string() } else { c }
}
})
.collect();
s = parts.join("/");
s = normalize_path(&s);
if s.starts_with("./") {
s = s[2..].to_string();
}
s.to_lowercase()
}
fn normalize_path(path: &str) -> String {
let mut result = Vec::new();
for part in path.split('/') {
match part {
"." | "" => continue,
".." => {
if !result.is_empty() {
result.pop();
}
}
_ => result.push(part),
}
}
let result = result.join("/");
if result.is_empty() {
".".to_string()
} else {
result
}
}
fn get_cwd_basename() -> String {
std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_lowercase()))
.unwrap_or_default()
}
fn resolve_cwd_reentry(normalized: &str) -> String {
if !normalized.starts_with("../") {
return normalized.to_string();
}
let cwd_base = get_cwd_basename();
if cwd_base.is_empty() {
return normalized.to_string();
}
let prefix = format!("../{}/", cwd_base);
let mut s = normalized.to_string();
while s.starts_with(&prefix) {
s = s[prefix.len()..].to_string();
}
let exact = format!("../{}", cwd_base);
if s == exact {
return ".".to_string();
}
s
}
fn matches_git_internal_prefix(n: &str) -> bool {
if n == "head" || n == ".git" {
return true;
}
if n.starts_with(".git/") || n.starts_with("git~") {
return true;
}
for p in GIT_INTERNAL_PREFIXES {
if p == &"head" {
continue;
}
if n == *p || n.starts_with(&format!("{}/", p)) {
return true;
}
}
false
}
fn matches_dot_git_prefix(n: &str) -> bool {
if n == ".git" || n.starts_with(".git/") {
return true;
}
regex::Regex::new(r"^git~\d+($|/)")
.ok()
.map(|re| re.is_match(n))
.unwrap_or(false)
}
fn resolve_escaping_path_to_cwd_relative(n: &str) -> Option<String> {
let cwd = std::env::current_dir().ok()?;
let cwd_str = cwd.to_string_lossy();
let resolved = cwd.join(n);
let resolved_str = resolved.to_string_lossy().to_lowercase();
let cwd_lower = cwd_str.to_lowercase();
if resolved_str == cwd_lower {
return Some(".".to_string());
}
let cwd_with_sep = if cwd_lower.ends_with('/') || cwd_lower.ends_with('\\') {
cwd_lower.clone()
} else {
format!("{}/", cwd_lower)
};
if !resolved_str.starts_with(&cwd_with_sep) {
return None;
}
let rel = &resolved_str[cwd_with_sep.len()..];
Some(rel.replace('\\', "/"))
}
pub fn is_git_internal_path_ps(arg: &str) -> bool {
let n = resolve_cwd_reentry(&normalize_git_path_arg(arg));
if matches_git_internal_prefix(&n) {
return true;
}
if n.starts_with("../") || n.starts_with('/') || n.len() >= 2 && n.chars().nth(1) == Some(':') {
if let Some(rel) = resolve_escaping_path_to_cwd_relative(&n) {
if matches_git_internal_prefix(&rel) {
return true;
}
}
}
false
}
pub fn is_dot_git_path_ps(arg: &str) -> bool {
let n = resolve_cwd_reentry(&normalize_git_path_arg(arg));
if matches_dot_git_prefix(&n) {
return true;
}
if n.starts_with("../") || n.starts_with('/') || n.len() >= 2 && n.chars().nth(1) == Some(':') {
if let Some(rel) = resolve_escaping_path_to_cwd_relative(&n) {
if matches_dot_git_prefix(&rel) {
return true;
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_git_path() {
assert_eq!(
normalize_git_path_arg("hooks/pre-commit"),
"hooks/pre-commit"
);
assert_eq!(normalize_git_path_arg(".git/hooks"), ".git/hooks");
}
}