use std::path::Path;
use crate::db::models::Project;
use crate::services::file::slugify_project_name;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirectoryIdentity {
pub match_directory: String,
pub remote: Option<String>,
}
impl DirectoryIdentity {
pub fn path_only(working_directory: &str) -> Self {
Self {
match_directory: working_directory.to_string(),
remote: None,
}
}
}
pub fn resolve_directory_identity(working_directory: &str) -> DirectoryIdentity {
match git_stdout(&["-C", working_directory, "rev-parse", "--show-toplevel"]) {
Some(toplevel) => {
let origin = git_stdout(&["-C", &toplevel, "remote", "get-url", "origin"]);
DirectoryIdentity {
remote: origin.as_deref().and_then(normalize_remote),
match_directory: toplevel,
}
}
None => DirectoryIdentity::path_only(working_directory),
}
}
fn git_stdout(args: &[&str]) -> Option<String> {
let out = std::process::Command::new("git").args(args).output().ok()?;
if !out.status.success() {
return None;
}
let text = String::from_utf8_lossy(&out.stdout).trim().to_string();
(!text.is_empty()).then_some(text)
}
pub fn normalize_remote(url: &str) -> Option<String> {
let mut s = url.trim().to_string();
if s.is_empty() {
return None;
}
let lower = s.to_ascii_lowercase();
for scheme in ["git+ssh://", "https://", "http://", "ssh://", "git://"] {
if let Some(rest) = lower.strip_prefix(scheme) {
s = rest.to_string();
break;
}
}
if let Some(at) = s.find('@')
&& !s[..at].contains('/')
{
s = s[at + 1..].to_string();
}
if let Some(colon) = s.find(':')
&& !s[..colon].contains('/')
{
s = format!("{}/{}", &s[..colon], &s[colon + 1..]);
}
while let Some(stripped) = s.strip_suffix('/') {
s = stripped.to_string();
}
if let Some(stripped) = s.strip_suffix(".git") {
s = stripped.to_string();
}
let (host, path) = s.split_once('/')?;
if host.is_empty() || path.is_empty() {
return None;
}
Some(format!("{}/{}", host.to_ascii_lowercase(), path))
}
fn directory_slug(working_directory: &str) -> Option<String> {
let trimmed = working_directory.trim_end_matches(['/', '\\']);
let name = Path::new(trimmed).file_name()?.to_str()?;
let slug = slugify_project_name(name);
(!slug.is_empty()).then_some(slug)
}
pub fn match_by_directory<'a>(
identity: &DirectoryIdentity,
projects: &'a [Project],
) -> Option<&'a Project> {
if let Some(remote) = identity.remote.as_deref()
&& let Some(hit) = projects
.iter()
.find(|p| p.repo_remote.as_deref() == Some(remote))
{
return Some(hit);
}
let dir = directory_slug(&identity.match_directory)?;
projects.iter().find(|p| {
slugify_project_name(&p.name) == dir
&& match (identity.remote.as_deref(), p.repo_remote.as_deref()) {
(Some(session), Some(project)) => session == project,
(None, Some(_)) => false,
_ => true,
}
})
}