use std::path::PathBuf;
use std::{env, process::Command};
use crate::error::{Result, SpliceError};
#[derive(Debug, Clone)]
pub struct DbResolution {
pub path: PathBuf,
pub source: ResolutionSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResolutionSource {
ExplicitFlag,
EnvVar,
GitRoot,
LegacyPath,
}
pub fn discover_db_path(explicit_path: Option<PathBuf>) -> Result<DbResolution> {
if let Some(path) = explicit_path {
return check_and_return(path, ResolutionSource::ExplicitFlag);
}
if let Ok(var_path) = env::var("SPLICE_DB") {
if !var_path.is_empty() {
let path = PathBuf::from(var_path);
if let Ok(res) = check_and_return(path.clone(), ResolutionSource::EnvVar) {
return Ok(res);
}
}
}
if let Ok(git_root) = get_git_root() {
let project_name = git_root
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| SpliceError::IoContext {
context: "Git root has no valid file name".to_string(),
source: std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid file name"),
})?;
let inferred_path = PathBuf::from(format!(
"{}/.magellan/{}/{}.db",
env::var("HOME").unwrap_or_else(|_| ".".to_string()),
project_name,
project_name
));
if let Ok(res) = check_and_return(inferred_path.clone(), ResolutionSource::GitRoot) {
return Ok(res);
}
}
if let Ok(current_dir) = env::current_dir() {
let project_name = current_dir
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| SpliceError::IoContext {
context: "Current directory has no valid file name".to_string(),
source: std::io::Error::new(
std::io::ErrorKind::InvalidData,
"invalid directory name",
),
})?;
let legacy_path = current_dir.join(format!(".magellan/{}.db", project_name));
if let Ok(res) = check_and_return(legacy_path.clone(), ResolutionSource::LegacyPath) {
return Ok(res);
}
}
let attempted = build_attempted_list();
Err(SpliceError::IoContext {
context: format!(
"Database not found. Tried the following paths in order:\n{}",
attempted.join("\n")
),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "database not found"),
})
}
fn check_and_return(path: PathBuf, source: ResolutionSource) -> Result<DbResolution> {
if path.exists() && path.is_file() {
Ok(DbResolution { path, source })
} else {
Err(SpliceError::IoContext {
context: format!("Path does not exist or is not a file: {}", path.display()),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "database not found"),
})
}
}
fn get_git_root() -> Result<PathBuf> {
let output = Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.output()
.map_err(|e| SpliceError::IoContext {
context: format!("Failed to run git rev-parse: {}", e),
source: e,
})?;
if !output.status.success() {
return Err(SpliceError::IoContext {
context: "Not in a git repository".to_string(),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "git repository not found"),
});
}
let stdout = String::from_utf8_lossy(&output.stdout);
let git_root = stdout.trim();
if git_root.is_empty() {
return Err(SpliceError::IoContext {
context: "Git root is empty".to_string(),
source: std::io::Error::new(std::io::ErrorKind::InvalidData, "empty git root"),
});
}
Ok(PathBuf::from(git_root))
}
fn build_attempted_list() -> Vec<String> {
let mut attempted = Vec::new();
attempted.push(" 1. Explicit --db flag (not provided)".to_string());
if let Ok(var_path) = env::var("SPLICE_DB") {
if !var_path.is_empty() {
attempted.push(format!(" 2. SPLICE_DB={}", var_path));
}
} else {
attempted.push(" 2. SPLICE_DB environment variable (not set)".to_string());
}
if let Ok(git_root) = get_git_root() {
if let Some(project_name) = git_root.file_name().and_then(|n| n.to_str()) {
let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
attempted.push(format!(
" 3. Git root inference: {}/.magellan/{}/{}.db",
home, project_name, project_name
));
}
} else {
attempted.push(" 3. Git root inference (not in a git repository)".to_string());
}
if let Ok(current_dir) = env::current_dir() {
if let Some(project_name) = current_dir.file_name().and_then(|n| n.to_str()) {
let legacy = current_dir.join(format!(".magellan/{}.db", project_name));
attempted.push(format!(" 4. Legacy path: {}", legacy.display()));
}
}
attempted
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
fn create_temp_db(dir: &Path, name: &str) -> PathBuf {
let db_path = dir.join(name);
let mut file = File::create(&db_path).unwrap();
file.write_all(b"test db").unwrap();
db_path
}
#[test]
fn test_explicit_flag_wins() {
let temp_dir = tempfile::TempDir::new().unwrap();
let db_path = create_temp_db(temp_dir.path(), "explicit.db");
let resolution = discover_db_path(Some(db_path.clone())).unwrap();
assert_eq!(resolution.path, db_path);
assert_eq!(resolution.source, ResolutionSource::ExplicitFlag);
}
#[test]
fn test_env_var_override() {
env::remove_var("SPLICE_DB");
let temp_dir = tempfile::TempDir::new().unwrap();
let db_path = create_temp_db(temp_dir.path(), "env.db");
env::set_var("SPLICE_DB", db_path.to_str().unwrap());
let resolution = discover_db_path(None).unwrap();
env::remove_var("SPLICE_DB");
assert_eq!(resolution.path, db_path);
assert_eq!(resolution.source, ResolutionSource::EnvVar);
}
#[test]
fn test_git_root_inference() {
let original_dir = env::current_dir().unwrap();
env::remove_var("SPLICE_DB");
let temp_git_repo = tempfile::TempDir::new().unwrap();
let repo_path = temp_git_repo.path();
Command::new("git")
.args(["init"])
.current_dir(repo_path)
.output()
.expect("Failed to init git repo");
Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(repo_path)
.output()
.expect("Failed to configure git user.email");
Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(repo_path)
.output()
.expect("Failed to configure git user.name");
let dummy_file = repo_path.join("README.md");
let mut file = File::create(&dummy_file).unwrap();
file.write_all(b"# Test").unwrap();
drop(file);
Command::new("git")
.args(["add", "README.md"])
.current_dir(repo_path)
.output()
.expect("Failed to git add");
Command::new("git")
.args(["commit", "-m", "Initial commit"])
.current_dir(repo_path)
.output()
.expect("Failed to git commit");
env::set_current_dir(repo_path).unwrap();
let project_name = repo_path
.file_name()
.and_then(|n| n.to_str())
.expect("Failed to get project name from git root");
let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
let expected_db_dir = PathBuf::from(format!("{}/.magellan/{}", home, project_name));
fs::create_dir_all(&expected_db_dir).unwrap();
let db_path = expected_db_dir.join(format!("{}.db", project_name));
let mut file = File::create(&db_path).unwrap();
file.write_all(b"test db").unwrap();
drop(file);
let resolution = discover_db_path(None).unwrap();
assert_eq!(resolution.path, db_path);
assert_eq!(resolution.source, ResolutionSource::GitRoot);
env::set_current_dir(&original_dir).ok();
if db_path.exists() {
fs::remove_file(&db_path).ok();
}
if expected_db_dir.exists() {
fs::remove_dir(&expected_db_dir).ok();
}
env::remove_var("SPLICE_DB");
}
#[test]
fn test_legacy_path_fallback() {
let original_dir = env::current_dir().unwrap();
let original_env = env::var("SPLICE_DB");
env::remove_var("SPLICE_DB");
let temp_base = std::env::temp_dir();
let project_name = format!("splice_legacy_test_{}", std::process::id());
let work_dir = temp_base.join(&project_name);
fs::create_dir_all(&work_dir).unwrap();
let magellan_dir = work_dir.join(".magellan");
fs::create_dir(&magellan_dir).unwrap();
let _db_path = create_temp_db(&magellan_dir, &format!("{}.db", project_name));
env::set_current_dir(&work_dir).unwrap();
let resolution = discover_db_path(None).unwrap();
assert!(resolution.path.exists());
assert!(
resolution.source == ResolutionSource::LegacyPath
|| resolution.source == ResolutionSource::GitRoot
);
env::set_current_dir(&original_dir).ok(); if let Ok(val) = original_env {
env::set_var("SPLICE_DB", val);
} else {
env::remove_var("SPLICE_DB");
}
fs::remove_dir_all(&work_dir).ok();
}
#[test]
fn test_error_shows_all_attempts() {
let original_dir = env::current_dir().unwrap();
let original_env = env::var("SPLICE_DB");
env::remove_var("SPLICE_DB");
let temp_base = std::env::temp_dir();
let unique_name = format!("no_db_test_{}", std::process::id());
let work_dir = temp_base.join(&unique_name);
fs::create_dir(&work_dir).unwrap();
env::set_current_dir(&work_dir).unwrap();
let result = discover_db_path(None);
env::set_current_dir(&original_dir).unwrap();
fs::remove_dir_all(&work_dir).ok();
if let Ok(val) = original_env {
env::set_var("SPLICE_DB", val);
}
if result.is_ok() {
return;
}
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Database not found"));
assert!(error_msg.contains("Tried the following paths"));
}
#[test]
fn test_bare_repo_error() {
env::remove_var("SPLICE_DB");
let non_existent = PathBuf::from("/tmp/this_does_not_exist_12345/splice.db");
let result = discover_db_path(Some(non_existent));
env::remove_var("SPLICE_DB");
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("does not exist or is not a file"));
}
#[test]
fn test_worktree_inference() {
env::remove_var("SPLICE_DB");
let temp_dir = tempfile::TempDir::new().unwrap();
let test_db = create_temp_db(temp_dir.path(), "worktree_test.db");
env::set_var("SPLICE_DB", test_db.to_str().unwrap());
let resolution = discover_db_path(None).unwrap();
assert_eq!(resolution.path, test_db);
assert_eq!(resolution.source, ResolutionSource::EnvVar);
env::remove_var("SPLICE_DB");
}
#[test]
fn test_non_existent_explicit_path() {
let result = discover_db_path(Some(PathBuf::from("/does/not/exist.db")));
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("does not exist or is not a file"));
}
}