use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use crate::service::registry::Registry;
pub fn resolve_db_path(explicit: Option<PathBuf>) -> Result<PathBuf> {
if let Some(path) = explicit {
return Ok(path);
}
let registry =
Registry::load().context("Failed to load project registry for default DB resolution")?;
let cwd = std::env::current_dir().context("Failed to get current working directory")?;
if let Some(entry) = registry.find_by_root(&cwd) {
let canon = Registry::canonical_db_path(&entry.name);
Registry::ensure_db_dir(&entry.name)?;
return Ok(canon);
}
if let Some(name) = detect_project_name_from_git(&cwd) {
let canon = Registry::canonical_db_path(&name);
Registry::ensure_db_dir(&name)?;
return Ok(canon);
}
Ok(PathBuf::from(".magellan/magellan.db"))
}
fn detect_project_name_from_git(start: &Path) -> Option<String> {
let repo = git2::Repository::discover(start).ok()?;
let repo_root = repo
.workdir()
.or_else(|| repo.path().parent())?
.to_path_buf();
if let Ok(remote) = repo.find_remote("origin") {
if let Ok(url) = remote.url() {
if let Some(name) = project_name_from_remote_url(url) {
return Some(name);
}
}
}
repo_root
.file_name()
.and_then(|n| n.to_str())
.map(sanitize_project_name)
}
fn project_name_from_remote_url(url: &str) -> Option<String> {
let trimmed = url.trim_end_matches('/').trim_end_matches(".git");
let without_scheme = trimmed
.split_once("://")
.map(|(_, rest)| rest)
.unwrap_or(trimmed);
let name = without_scheme
.rsplit_once('/')
.or_else(|| without_scheme.rsplit_once(':'))
.map(|(_, name)| name)?;
if name.is_empty() {
return None;
}
if !without_scheme.contains('/') && !without_scheme.contains(':') {
return None;
}
Some(sanitize_project_name(name))
}
fn sanitize_project_name(name: &str) -> String {
name.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' || c == '.' {
c
} else {
'-'
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_project_name_from_remote_url_https() {
assert_eq!(
project_name_from_remote_url("https://github.com/oldnordic/magellan.git"),
Some("magellan".to_string())
);
}
#[test]
fn test_project_name_from_remote_url_ssh() {
assert_eq!(
project_name_from_remote_url("git@github.com:oldnordic/magellan.git"),
Some("magellan".to_string())
);
}
#[test]
fn test_project_name_from_remote_url_no_suffix() {
assert_eq!(
project_name_from_remote_url("https://github.com/oldnordic/magellan"),
Some("magellan".to_string())
);
}
#[test]
fn test_project_name_from_remote_url_file() {
assert_eq!(
project_name_from_remote_url("file:///home/user/projects/magellan.git"),
Some("magellan".to_string())
);
}
#[test]
fn test_project_name_from_remote_url_trailing_slash() {
assert_eq!(
project_name_from_remote_url("https://github.com/oldnordic/magellan.git/"),
Some("magellan".to_string())
);
}
#[test]
fn test_project_name_from_remote_url_invalid() {
assert_eq!(project_name_from_remote_url(""), None);
assert_eq!(project_name_from_remote_url("https://github.com/"), None);
}
#[test]
fn test_sanitize_project_name() {
assert_eq!(sanitize_project_name("my_project"), "my_project");
assert_eq!(sanitize_project_name("my/project"), "my-project");
assert_eq!(sanitize_project_name("my project"), "my-project");
assert_eq!(sanitize_project_name("foo..bar"), "foo..bar");
}
#[test]
fn test_detect_project_name_from_git_uses_origin_remote() {
let tmp = tempfile::tempdir().unwrap();
let repo_dir = tmp.path().join("my-repo");
std::fs::create_dir(&repo_dir).unwrap();
let repo = git2::Repository::init(&repo_dir).unwrap();
repo.remote("origin", "https://github.com/user/remote-name.git")
.unwrap();
let src_dir = repo_dir.join("src");
std::fs::create_dir(&src_dir).unwrap();
let name = detect_project_name_from_git(&src_dir);
assert_eq!(name, Some("remote-name".to_string()));
}
#[test]
fn test_detect_project_name_from_git_falls_back_to_dir_name() {
let tmp = tempfile::tempdir().unwrap();
let repo_dir = tmp.path().join("local-name");
git2::Repository::init(&repo_dir).unwrap();
let name = detect_project_name_from_git(&repo_dir);
assert_eq!(name, Some("local-name".to_string()));
}
#[test]
fn test_detect_project_name_from_git_outside_repo() {
let tmp = tempfile::tempdir().unwrap();
assert!(detect_project_name_from_git(tmp.path()).is_none());
}
#[test]
fn test_resolve_db_path_explicit() {
assert_eq!(
resolve_db_path(Some(PathBuf::from("custom.db"))).unwrap(),
PathBuf::from("custom.db")
);
}
}