use super::daemon_utils::daemon_base_url;
use super::reindex_engine::{
register_index_with_daemon, register_index_with_daemon_filtered, run_reindex,
run_reindex_force, RegisterFilters,
};
use crate::config::{CollectionConfig, GlobalConfig};
use crate::core::project_config::{ProjectConfig, PROJECT_CONFIG_FILENAME};
use crate::core::repo_config::{language_to_exts, IndexConfig, RepoConfig, CONFIG_FILENAME};
use anyhow::Result;
use colored::Colorize;
pub async fn handle_index(
cli_path: Option<std::path::PathBuf>,
cli_name: Option<String>,
force: bool,
cli_exclude: Vec<String>,
timeout_secs: u64,
) -> Result<()> {
let cwd = std::env::current_dir().unwrap_or_default();
let project_cfg = match ProjectConfig::load(&cwd) {
Ok(Some(cfg)) => {
tracing::debug!(
"loaded {} from {}: name={:?} path={:?} exclude={:?}",
PROJECT_CONFIG_FILENAME,
cwd.display(),
cfg.name,
cfg.path,
cfg.exclude,
);
Some(cfg)
}
Ok(None) => None,
Err(e) => anyhow::bail!("could not parse {}: {e}", PROJECT_CONFIG_FILENAME),
};
let project_path = resolve_project_path(cli_path, project_cfg.as_ref(), &cwd);
crate::commands::daemon_guard::ensure_daemon_running_for_indexing(&daemon_base_url()).await?;
match RepoConfig::load(&project_path) {
Ok(Some(cfg)) => {
println!(
"{} loaded {} ({} index{} declared)",
"→".cyan(),
CONFIG_FILENAME.bold(),
cfg.indexes.len(),
if cfg.indexes.len() == 1 { "" } else { "es" },
);
if cli_name.is_some() {
eprintln!(
"{} --name is ignored when {} is present",
"ℹ".yellow(),
CONFIG_FILENAME
);
}
for idx in &cfg.indexes {
let filters = filters_from_index_config(idx);
index_one_with_filters(&idx.name, &project_path, force, timeout_secs, &filters)
.await?;
}
return Ok(());
}
Ok(None) => {
}
Err(e) => {
anyhow::bail!("could not parse {}: {e}", CONFIG_FILENAME);
}
}
let index_name = resolve_index_name(cli_name, project_cfg.as_ref(), &project_path);
let exclude_globs = resolve_excludes(cli_exclude, project_cfg.as_ref());
if exclude_globs.is_empty() {
index_one(&index_name, &project_path, force, timeout_secs).await
} else {
let filters = RegisterFilters {
exclude_globs,
..RegisterFilters::default()
};
index_one_with_filters(&index_name, &project_path, force, timeout_secs, &filters).await
}
}
fn resolve_project_path(
cli_path: Option<std::path::PathBuf>,
cfg: Option<&ProjectConfig>,
config_dir: &std::path::Path,
) -> std::path::PathBuf {
if let Some(p) = cli_path {
return p;
}
if let Some(rel) = cfg.and_then(|c| c.path.as_ref()) {
return config_dir.join(rel);
}
config_dir.to_path_buf()
}
fn resolve_index_name(
cli_name: Option<String>,
cfg: Option<&ProjectConfig>,
project_path: &std::path::Path,
) -> String {
if let Some(n) = cli_name {
return n;
}
if let Some(n) = cfg.and_then(|c| c.name.clone()) {
return n;
}
project_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned()
}
fn resolve_excludes(cli_exclude: Vec<String>, cfg: Option<&ProjectConfig>) -> Vec<String> {
if !cli_exclude.is_empty() {
return cli_exclude;
}
cfg.and_then(|c| c.exclude.clone()).unwrap_or_default()
}
pub(crate) fn filters_from_index_config(idx: &IndexConfig) -> RegisterFilters {
let mut extensions: Vec<String> = Vec::new();
for lang in &idx.languages {
for e in language_to_exts(lang) {
extensions.push((*e).to_string());
}
}
extensions.sort();
extensions.dedup();
RegisterFilters {
include_paths: idx.paths.clone(),
exclude_globs: idx.exclude.clone(),
extensions,
domain_terms: idx.domain_terms.clone(),
}
}
async fn index_one(
index_name: &str,
project_path: &std::path::Path,
force: bool,
timeout_secs: u64,
) -> Result<()> {
index_one_with_filters(
index_name,
project_path,
force,
timeout_secs,
&RegisterFilters::default(),
)
.await
}
async fn index_one_with_filters(
index_name: &str,
project_path: &std::path::Path,
force: bool,
timeout_secs: u64,
filters: &RegisterFilters,
) -> Result<()> {
let result = if filters.include_paths.is_empty()
&& filters.exclude_globs.is_empty()
&& filters.extensions.is_empty()
&& filters.domain_terms.is_empty()
{
register_index_with_daemon(index_name, project_path).await
} else {
register_index_with_daemon_filtered(index_name, project_path, filters).await
};
let (created, daemon_reachable) = result?;
if !daemon_reachable {
anyhow::bail!(
"Daemon not reachable at {}. Start it with `trusty-search start`.",
daemon_base_url(),
);
}
if created {
println!(
"{} '{}' registered at {}",
"✓".green(),
index_name.bold(),
project_path.display()
);
}
persist_collection_to_global_config(index_name, project_path, filters);
if force {
run_reindex_force(index_name, project_path, timeout_secs).await?;
} else {
run_reindex(index_name, project_path, timeout_secs).await?;
}
Ok(())
}
fn persist_collection_to_global_config(
index_name: &str,
project_path: &std::path::Path,
filters: &RegisterFilters,
) {
let mut cfg = match GlobalConfig::load() {
Ok(c) => c,
Err(e) => {
tracing::warn!("could not load global config to record index '{index_name}': {e:#}");
return;
}
};
cfg.upsert_collection(CollectionConfig {
name: index_name.to_string(),
path: project_path.to_path_buf(),
extensions: filters.extensions.clone(),
exclude: filters.exclude_globs.clone(),
domain_terms: filters.domain_terms.clone(),
});
if let Err(e) = cfg.save() {
tracing::warn!("could not save global config after registering '{index_name}': {e:#}");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::project_config::ProjectConfig;
use std::path::{Path, PathBuf};
fn cfg(name: Option<&str>, path: Option<&str>, exclude: Option<Vec<&str>>) -> ProjectConfig {
ProjectConfig {
name: name.map(str::to_string),
path: path.map(PathBuf::from),
exclude: exclude.map(|v| v.into_iter().map(str::to_string).collect()),
}
}
#[test]
fn merge_path_cli_wins() {
let c = cfg(None, Some("app"), None);
let got = resolve_project_path(
Some(PathBuf::from("/explicit/cli")),
Some(&c),
Path::new("/repo"),
);
assert_eq!(got, PathBuf::from("/explicit/cli"));
}
#[test]
fn merge_path_config_relative() {
let c = cfg(None, Some("app"), None);
let got = resolve_project_path(None, Some(&c), Path::new("/repo"));
assert_eq!(got, PathBuf::from("/repo/app"));
}
#[test]
fn merge_path_default_is_cwd() {
let got = resolve_project_path(None, None, Path::new("/repo"));
assert_eq!(got, PathBuf::from("/repo"));
}
#[test]
fn merge_path_config_present_but_no_path_field() {
let c = cfg(Some("foo"), None, None);
let got = resolve_project_path(None, Some(&c), Path::new("/repo"));
assert_eq!(got, PathBuf::from("/repo"));
}
#[test]
fn merge_name_cli_wins() {
let c = cfg(Some("from-config"), None, None);
let got = resolve_index_name(Some("from-cli".into()), Some(&c), Path::new("/repo/myproj"));
assert_eq!(got, "from-cli");
}
#[test]
fn merge_name_from_config() {
let c = cfg(Some("from-config"), None, None);
let got = resolve_index_name(None, Some(&c), Path::new("/repo/myproj"));
assert_eq!(got, "from-config");
}
#[test]
fn merge_name_default_is_basename() {
let got = resolve_index_name(None, None, Path::new("/repo/myproj"));
assert_eq!(got, "myproj");
}
#[test]
fn merge_exclude_cli_wins() {
let c = cfg(None, None, Some(vec!["data/", "docs/"]));
let got = resolve_excludes(vec!["only-cli/".into()], Some(&c));
assert_eq!(got, vec!["only-cli/".to_string()]);
}
#[test]
fn merge_exclude_from_config() {
let c = cfg(None, None, Some(vec!["data/", "*.db"]));
let got = resolve_excludes(Vec::new(), Some(&c));
assert_eq!(got, vec!["data/".to_string(), "*.db".to_string()]);
}
#[test]
fn merge_exclude_default_empty() {
assert!(resolve_excludes(Vec::new(), None).is_empty());
let c = cfg(Some("foo"), None, None);
assert!(resolve_excludes(Vec::new(), Some(&c)).is_empty());
}
}