use domain::error::Result;
use domain::use_cases::index::IndexUseCase;
use storage::SqliteStore;
use crate::adapters::fs::RealFileSystem;
use crate::adapters::git::ShellGitProvider;
use crate::adapters::parse::RayonParseProvider;
use crate::output::{print, OutputFormat};
use crate::project::{ensure_data_dir, find_project_root};
use super::IndexArgs;
pub fn run_index(args: &IndexArgs, output_format: OutputFormat) -> Result<()> {
let root = match &args.path {
Some(p) => p.clone(),
None => find_project_root(&std::env::current_dir().map_err(|e| {
domain::error::CodeGraphError::FileSystem {
path: ".".into(),
source: e,
}
})?)?,
};
let data_dir = ensure_data_dir(&root)?;
let db_path = data_dir.join("graph.db");
let store = SqliteStore::open(&db_path)
.map_err(|e| domain::error::CodeGraphError::Storage(format!("{e}")))?;
let fs = RealFileSystem;
let git = ShellGitProvider::new(root.clone());
let parser = RayonParseProvider::new();
let use_case = IndexUseCase::new(store.clone(), parser, fs, git);
let stats = if let Some(files) = &args.files {
use_case.incremental_files(&root, files.clone())?
} else if args.incremental {
use_case.incremental_index(&root)?
} else {
use_case.full_index(&root)?
};
print(&stats, output_format);
if args.embed {
#[cfg(feature = "embeddings")]
{
let config = crate::config::load_config(&root)?;
let ep = embeddings::OnnxEmbeddingProvider::from_model_name(&args.embed_model, 384)
.map_err(|e| domain::error::CodeGraphError::Other(e.to_string()))?;
let embed_config = domain::model::EmbeddingConfig {
model: args.embed_model.clone(),
batch_size: config
.embeddings
.as_ref()
.and_then(|e| e.batch_size)
.unwrap_or(64),
};
let embed_uc =
domain::use_cases::embed::EmbedUseCase::new(store.clone(), ep, store.clone());
let pb = indicatif::ProgressBar::new(0);
pb.set_style(
indicatif::ProgressStyle::default_bar()
.template("{spinner:.green} Embedding [{bar:40.cyan/blue}] {pos}/{len} symbols")
.unwrap()
.progress_chars("#>-"),
);
pb.set_draw_target(indicatif::ProgressDrawTarget::stderr());
let embed_stats = embed_uc.embed_all(&embed_config, |done, total| {
pb.set_length(total as u64);
pb.set_position(done as u64);
})?;
pb.finish_and_clear();
let removed = embed_uc.cleanup_orphans()?;
let embed_stats = domain::model::EmbedStats {
removed,
..embed_stats
};
print(&embed_stats, output_format);
}
#[cfg(not(feature = "embeddings"))]
{
return Err(domain::error::CodeGraphError::Other(
"--embed requires the 'embeddings' feature; rebuild with `cargo build --features embeddings`".into(),
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn git(root: &std::path::Path) -> std::process::Command {
let mut cmd = std::process::Command::new("git");
cmd.current_dir(root)
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.env_remove("GIT_INDEX_FILE");
cmd
}
fn setup_git_repo(root: &std::path::Path) {
git(root).args(["init"]).output().unwrap();
git(root)
.args(["config", "user.email", "test@test.com"])
.output()
.unwrap();
git(root)
.args(["config", "user.name", "Test"])
.output()
.unwrap();
}
#[test]
fn index_on_fixture_project_creates_db() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
setup_git_repo(root);
let src = root.join("src");
fs::create_dir_all(&src).unwrap();
fs::write(
src.join("main.ts"),
"export function hello(): void {}\nexport class Greeter {}",
)
.unwrap();
fs::write(
src.join("util.ts"),
"export function add(a: number, b: number): number { return a + b; }",
)
.unwrap();
let args = IndexArgs {
path: Some(root.to_path_buf()),
incremental: false,
files: None,
embed: false,
embed_model: "all-MiniLM-L6-v2".into(),
};
let result = run_index(&args, OutputFormat::Compact);
assert!(result.is_ok(), "index failed: {:?}", result.err());
let db_path = root.join(".code-graph").join("graph.db");
assert!(db_path.exists(), "graph.db should exist");
}
#[test]
fn index_incremental_updates_changed_files() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
setup_git_repo(root);
let src = root.join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("main.ts"), "export function hello(): void {}").unwrap();
let args = IndexArgs {
path: Some(root.to_path_buf()),
incremental: false,
files: None,
embed: false,
embed_model: "all-MiniLM-L6-v2".into(),
};
run_index(&args, OutputFormat::Compact).unwrap();
git(root).args(["add", "."]).output().unwrap();
git(root)
.args(["commit", "-m", "initial"])
.output()
.unwrap();
fs::write(
src.join("main.ts"),
"export function hello(): void {}\nexport function world(): void {}",
)
.unwrap();
let args = IndexArgs {
path: Some(root.to_path_buf()),
incremental: true,
files: None,
embed: false,
embed_model: "all-MiniLM-L6-v2".into(),
};
let result = run_index(&args, OutputFormat::Compact);
assert!(
result.is_ok(),
"incremental index failed: {:?}",
result.err()
);
}
#[test]
fn index_incremental_with_no_changes() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
setup_git_repo(root);
let src = root.join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("main.ts"), "export function hello(): void {}").unwrap();
let args = IndexArgs {
path: Some(root.to_path_buf()),
incremental: false,
files: None,
embed: false,
embed_model: "all-MiniLM-L6-v2".into(),
};
run_index(&args, OutputFormat::Compact).unwrap();
git(root).args(["add", "."]).output().unwrap();
git(root)
.args(["commit", "-m", "initial"])
.output()
.unwrap();
let args = IndexArgs {
path: Some(root.to_path_buf()),
incremental: true,
files: None,
embed: false,
embed_model: "all-MiniLM-L6-v2".into(),
};
let result = run_index(&args, OutputFormat::Compact);
assert!(
result.is_ok(),
"incremental no-op failed: {:?}",
result.err()
);
}
#[test]
fn index_files_updates_specific_files() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
setup_git_repo(root);
let src = root.join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("main.ts"), "export function hello(): void {}").unwrap();
let args = IndexArgs {
path: Some(root.to_path_buf()),
incremental: false,
files: None,
embed: false,
embed_model: "all-MiniLM-L6-v2".into(),
};
run_index(&args, OutputFormat::Compact).unwrap();
fs::write(
src.join("main.ts"),
"export function hello(): void {}\nexport function bar(): void {}",
)
.unwrap();
let args = IndexArgs {
path: Some(root.to_path_buf()),
incremental: false,
files: Some(vec![std::path::PathBuf::from("src/main.ts")]),
embed: false,
embed_model: "all-MiniLM-L6-v2".into(),
};
let result = run_index(&args, OutputFormat::Compact);
assert!(result.is_ok(), "index --files failed: {:?}", result.err());
}
}