use anyhow::Result;
use colored::Colorize;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use crate::constants::{CONFIG_DIR_NAME, DB_DIR_NAME, REPOS_CONFIG_FILE};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseInfo {
pub project_path: PathBuf,
pub db_path: PathBuf,
pub is_current: bool,
pub depth: usize,
pub is_global: bool,
}
pub fn is_valid_database(db_path: &Path) -> bool {
if !db_path.exists() || !db_path.is_dir() {
return false;
}
let metadata_exists = db_path.join("metadata.json").exists();
let lmdb_exists = db_path.join("data.mdb").exists(); let fts_exists = db_path.join("fts").is_dir();
metadata_exists && lmdb_exists && fts_exists
}
pub fn check_database_integrity(db_path: &Path) -> Option<String> {
if !db_path.exists() {
return None; }
if !db_path.is_dir() {
return Some("exists but is not a directory".to_string());
}
let mut missing = Vec::new();
if !db_path.join("metadata.json").exists() {
missing.push("metadata.json");
}
if !db_path.join("data.mdb").exists() {
missing.push("data.mdb");
}
if !db_path.join("fts").is_dir() {
missing.push("fts/");
}
if missing.is_empty() {
None } else {
Some(format!("missing: {}", missing.join(", ")))
}
}
pub fn find_databases() -> Result<Vec<DatabaseInfo>> {
let mut databases = Vec::new();
let current_dir = std::env::current_dir()?;
let current_db = current_dir.join(DB_DIR_NAME);
if current_db.exists() {
if is_valid_database(¤t_db) {
databases.push(DatabaseInfo {
project_path: current_dir.clone(),
db_path: current_db,
is_current: true,
depth: 0,
is_global: false,
});
} else if let Some(reason) = check_database_integrity(¤t_db) {
eprintln!(
"{}",
format!(
"⚠️ Skipping incomplete database at {}: {}",
current_db.display(),
reason
)
.yellow()
);
}
}
let mut parent_dir = current_dir.clone();
for depth in 1..=5 {
if let Some(parent) = parent_dir.parent() {
parent_dir = parent.to_path_buf();
let parent_db = parent_dir.join(DB_DIR_NAME);
if parent_db.exists() {
if is_valid_database(&parent_db) {
databases.push(DatabaseInfo {
project_path: parent_dir.clone(),
db_path: parent_db,
is_current: false,
depth,
is_global: false,
});
} else if let Some(reason) = check_database_integrity(&parent_db) {
eprintln!(
"{}",
format!(
"⚠️ Skipping incomplete database at {}: {}",
parent_db.display(),
reason
)
.yellow()
);
}
}
} else {
break; }
}
if let Ok(global_dbs) = find_global_databases() {
databases.extend(global_dbs);
}
Ok(databases)
}
pub fn find_best_database(target_dir: Option<&Path>) -> Result<Option<DatabaseInfo>> {
let target = target_dir.unwrap_or_else(|| Path::new("."));
let canonical = if target.is_absolute() {
target.to_path_buf()
} else {
std::env::current_dir()?.join(target)
};
let canonical = match canonical.canonicalize() {
Ok(path) => path,
Err(_) => return Ok(None), };
let current_db = canonical.join(DB_DIR_NAME);
if current_db.exists() {
if is_valid_database(¤t_db) {
return Ok(Some(DatabaseInfo {
project_path: canonical.clone(),
db_path: current_db,
is_current: true,
depth: 0,
is_global: false,
}));
} else if let Some(reason) = check_database_integrity(¤t_db) {
eprintln!(
"{}",
format!(
"⚠️ Found incomplete database at {}: {}",
current_db.display(),
reason
)
.yellow()
);
eprintln!(
"{}",
" Run 'codesearch index --force' to rebuild it.".yellow()
);
}
}
let mut parent_dir = canonical.clone();
for depth in 1..=5 {
if let Some(parent) = parent_dir.parent() {
parent_dir = parent.to_path_buf();
let parent_db = parent_dir.join(DB_DIR_NAME);
if parent_db.exists() {
if is_valid_database(&parent_db) {
return Ok(Some(DatabaseInfo {
project_path: parent_dir.clone(),
db_path: parent_db,
is_current: false,
depth,
is_global: false,
}));
} else if let Some(reason) = check_database_integrity(&parent_db) {
eprintln!(
"{}",
format!(
"⚠️ Found incomplete database at {}: {}",
parent_db.display(),
reason
)
.yellow()
);
}
}
} else {
break;
}
}
let global_dbs = find_global_databases()?;
if !global_dbs.is_empty() {
return Ok(Some(global_dbs.into_iter().next().unwrap()));
}
Ok(None)
}
fn find_global_databases() -> Result<Vec<DatabaseInfo>> {
let home_dir = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("No home directory found"))?;
let config_dir = home_dir.join(CONFIG_DIR_NAME);
let config_path = config_dir.join(REPOS_CONFIG_FILE);
if !config_path.exists() {
return Ok(Vec::new());
}
let content = fs::read_to_string(&config_path)?;
let repos_map: HashMap<String, serde_json::Value> = serde_json::from_str(&content)?;
let mut databases = Vec::new();
for (project_path, _meta) in repos_map {
let path = PathBuf::from(&project_path);
let db_path = path.join(DB_DIR_NAME);
if is_valid_database(&db_path) {
databases.push(DatabaseInfo {
project_path: path,
db_path,
is_current: false,
depth: usize::MAX, is_global: true,
});
}
}
Ok(databases)
}
pub fn register_repository(project_path: &Path) -> Result<()> {
let home_dir = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("No home directory found"))?;
let config_dir = home_dir.join(CONFIG_DIR_NAME);
let config_path = config_dir.join(REPOS_CONFIG_FILE);
fs::create_dir_all(&config_dir)?;
let mut repos_map: HashMap<String, serde_json::Value> = if config_path.exists() {
let content = fs::read_to_string(&config_path)?;
serde_json::from_str(&content).unwrap_or_default()
} else {
HashMap::new()
};
let canonical_path = project_path.canonicalize()?;
let path_str = canonical_path.to_string_lossy().to_string();
repos_map.insert(
path_str.clone(),
serde_json::json!({
"indexed_at": chrono::Utc::now().to_rfc3339(),
}),
);
fs::write(&config_path, serde_json::to_string_pretty(&repos_map)?)?;
Ok(())
}
pub fn unregister_repository(project_path: &Path) -> Result<()> {
let home_dir = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("No home directory found"))?;
let config_dir = home_dir.join(CONFIG_DIR_NAME);
let config_path = config_dir.join(REPOS_CONFIG_FILE);
if !config_path.exists() {
return Ok(()); }
let content = fs::read_to_string(&config_path)?;
let mut repos_map: HashMap<String, serde_json::Value> = serde_json::from_str(&content)?;
let canonical_path = project_path.canonicalize()?;
let path_str = canonical_path.to_string_lossy().to_string();
repos_map.remove(&path_str);
fs::write(&config_path, serde_json::to_string_pretty(&repos_map)?)?;
Ok(())
}
pub fn resolve_database_with_message(
path: Option<&Path>,
action: &str,
) -> Result<(PathBuf, PathBuf)> {
let target = path.unwrap_or(Path::new("."));
if let Some(db_info) = find_best_database(Some(target))? {
let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
if !db_info.is_current {
let relative_path = if let Ok(rel) = current_dir.strip_prefix(&db_info.project_path) {
format!("./{}", rel.display())
} else {
db_info.project_path.display().to_string()
};
eprintln!(
"{}",
format!(
"📂 Using database from: {}\n ({} from subfolder, project root: {})",
db_info.db_path.display(),
action,
relative_path
)
.dimmed()
);
}
return Ok((db_info.db_path, db_info.project_path));
}
let project_path = if let Some(p) = path {
p.to_path_buf()
} else {
PathBuf::from(".")
};
let canonical_path = project_path.canonicalize().unwrap_or(project_path.clone());
let db_path = canonical_path.join(".codesearch.db");
Ok((db_path, canonical_path))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_databases() {
let databases = find_databases();
assert!(databases.is_ok());
let dbs = databases.unwrap();
println!("Found {} databases", dbs.len());
}
}