the_code_graph_cli/commands/
index.rs1use domain::error::Result;
2use domain::use_cases::index::IndexUseCase;
3use storage::SqliteStore;
4
5use crate::adapters::fs::RealFileSystem;
6use crate::adapters::git::ShellGitProvider;
7use crate::adapters::parse::RayonParseProvider;
8use crate::output::{print, OutputFormat};
9use crate::project::{ensure_data_dir, find_project_root};
10
11use super::IndexArgs;
12
13pub fn run_index(args: &IndexArgs, output_format: OutputFormat) -> Result<()> {
14 let root = match &args.path {
15 Some(p) => p.clone(),
16 None => find_project_root(&std::env::current_dir().map_err(|e| {
17 domain::error::CodeGraphError::FileSystem {
18 path: ".".into(),
19 source: e,
20 }
21 })?)?,
22 };
23
24 let data_dir = ensure_data_dir(&root)?;
25 let db_path = data_dir.join("graph.db");
26 let store = SqliteStore::open(&db_path)
27 .map_err(|e| domain::error::CodeGraphError::Storage(format!("{e}")))?;
28
29 let fs = RealFileSystem;
30 let git = ShellGitProvider::new(root.clone());
31 let parser = RayonParseProvider::new();
32
33 let use_case = IndexUseCase::new(store.clone(), parser, fs, git);
34 let stats = if let Some(files) = &args.files {
35 use_case.incremental_files(&root, files.clone())?
36 } else if args.incremental {
37 use_case.incremental_index(&root)?
38 } else {
39 use_case.full_index(&root)?
40 };
41
42 print(&stats, output_format);
43
44 if args.embed {
45 #[cfg(feature = "embeddings")]
46 {
47 let config = crate::config::load_config(&root)?;
48 let ep = embeddings::OnnxEmbeddingProvider::from_model_name(&args.embed_model, 384)
49 .map_err(|e| domain::error::CodeGraphError::Other(e.to_string()))?;
50
51 let embed_config = domain::model::EmbeddingConfig {
52 model: args.embed_model.clone(),
53 batch_size: config
54 .embeddings
55 .as_ref()
56 .and_then(|e| e.batch_size)
57 .unwrap_or(64),
58 };
59
60 let embed_uc =
61 domain::use_cases::embed::EmbedUseCase::new(store.clone(), ep, store.clone());
62
63 let pb = indicatif::ProgressBar::new(0);
64 pb.set_style(
65 indicatif::ProgressStyle::default_bar()
66 .template("{spinner:.green} Embedding [{bar:40.cyan/blue}] {pos}/{len} symbols")
67 .unwrap()
68 .progress_chars("#>-"),
69 );
70 pb.set_draw_target(indicatif::ProgressDrawTarget::stderr());
71
72 let embed_stats = embed_uc.embed_all(&embed_config, |done, total| {
73 pb.set_length(total as u64);
74 pb.set_position(done as u64);
75 })?;
76 pb.finish_and_clear();
77
78 let removed = embed_uc.cleanup_orphans()?;
79 let embed_stats = domain::model::EmbedStats {
80 removed,
81 ..embed_stats
82 };
83 print(&embed_stats, output_format);
84 }
85 #[cfg(not(feature = "embeddings"))]
86 {
87 return Err(domain::error::CodeGraphError::Other(
88 "--embed requires the 'embeddings' feature; rebuild with `cargo build --features embeddings`".into(),
89 ));
90 }
91 }
92
93 Ok(())
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use std::fs;
100
101 fn git(root: &std::path::Path) -> std::process::Command {
105 let mut cmd = std::process::Command::new("git");
106 cmd.current_dir(root)
107 .env_remove("GIT_DIR")
108 .env_remove("GIT_WORK_TREE")
109 .env_remove("GIT_INDEX_FILE");
110 cmd
111 }
112
113 fn setup_git_repo(root: &std::path::Path) {
114 git(root).args(["init"]).output().unwrap();
115 git(root)
116 .args(["config", "user.email", "test@test.com"])
117 .output()
118 .unwrap();
119 git(root)
120 .args(["config", "user.name", "Test"])
121 .output()
122 .unwrap();
123 }
124
125 #[test]
126 fn index_on_fixture_project_creates_db() {
127 let tmp = tempfile::tempdir().unwrap();
128 let root = tmp.path();
129 setup_git_repo(root);
130
131 let src = root.join("src");
132 fs::create_dir_all(&src).unwrap();
133 fs::write(
134 src.join("main.ts"),
135 "export function hello(): void {}\nexport class Greeter {}",
136 )
137 .unwrap();
138 fs::write(
139 src.join("util.ts"),
140 "export function add(a: number, b: number): number { return a + b; }",
141 )
142 .unwrap();
143
144 let args = IndexArgs {
145 path: Some(root.to_path_buf()),
146 incremental: false,
147 files: None,
148 embed: false,
149 embed_model: "all-MiniLM-L6-v2".into(),
150 };
151 let result = run_index(&args, OutputFormat::Compact);
152 assert!(result.is_ok(), "index failed: {:?}", result.err());
153
154 let db_path = root.join(".code-graph").join("graph.db");
155 assert!(db_path.exists(), "graph.db should exist");
156 }
157
158 #[test]
159 fn index_incremental_updates_changed_files() {
160 let tmp = tempfile::tempdir().unwrap();
161 let root = tmp.path();
162 setup_git_repo(root);
163
164 let src = root.join("src");
165 fs::create_dir_all(&src).unwrap();
166 fs::write(src.join("main.ts"), "export function hello(): void {}").unwrap();
167
168 let args = IndexArgs {
170 path: Some(root.to_path_buf()),
171 incremental: false,
172 files: None,
173 embed: false,
174 embed_model: "all-MiniLM-L6-v2".into(),
175 };
176 run_index(&args, OutputFormat::Compact).unwrap();
177
178 git(root).args(["add", "."]).output().unwrap();
180 git(root)
181 .args(["commit", "-m", "initial"])
182 .output()
183 .unwrap();
184
185 fs::write(
187 src.join("main.ts"),
188 "export function hello(): void {}\nexport function world(): void {}",
189 )
190 .unwrap();
191
192 let args = IndexArgs {
194 path: Some(root.to_path_buf()),
195 incremental: true,
196 files: None,
197 embed: false,
198 embed_model: "all-MiniLM-L6-v2".into(),
199 };
200 let result = run_index(&args, OutputFormat::Compact);
201 assert!(
202 result.is_ok(),
203 "incremental index failed: {:?}",
204 result.err()
205 );
206 }
207
208 #[test]
209 fn index_incremental_with_no_changes() {
210 let tmp = tempfile::tempdir().unwrap();
211 let root = tmp.path();
212 setup_git_repo(root);
213
214 let src = root.join("src");
215 fs::create_dir_all(&src).unwrap();
216 fs::write(src.join("main.ts"), "export function hello(): void {}").unwrap();
217
218 let args = IndexArgs {
220 path: Some(root.to_path_buf()),
221 incremental: false,
222 files: None,
223 embed: false,
224 embed_model: "all-MiniLM-L6-v2".into(),
225 };
226 run_index(&args, OutputFormat::Compact).unwrap();
227
228 git(root).args(["add", "."]).output().unwrap();
230 git(root)
231 .args(["commit", "-m", "initial"])
232 .output()
233 .unwrap();
234
235 let args = IndexArgs {
237 path: Some(root.to_path_buf()),
238 incremental: true,
239 files: None,
240 embed: false,
241 embed_model: "all-MiniLM-L6-v2".into(),
242 };
243 let result = run_index(&args, OutputFormat::Compact);
244 assert!(
245 result.is_ok(),
246 "incremental no-op failed: {:?}",
247 result.err()
248 );
249 }
250
251 #[test]
252 fn index_files_updates_specific_files() {
253 let tmp = tempfile::tempdir().unwrap();
254 let root = tmp.path();
255 setup_git_repo(root);
256
257 let src = root.join("src");
258 fs::create_dir_all(&src).unwrap();
259 fs::write(src.join("main.ts"), "export function hello(): void {}").unwrap();
260
261 let args = IndexArgs {
263 path: Some(root.to_path_buf()),
264 incremental: false,
265 files: None,
266 embed: false,
267 embed_model: "all-MiniLM-L6-v2".into(),
268 };
269 run_index(&args, OutputFormat::Compact).unwrap();
270
271 fs::write(
273 src.join("main.ts"),
274 "export function hello(): void {}\nexport function bar(): void {}",
275 )
276 .unwrap();
277
278 let args = IndexArgs {
280 path: Some(root.to_path_buf()),
281 incremental: false,
282 files: Some(vec![std::path::PathBuf::from("src/main.ts")]),
283 embed: false,
284 embed_model: "all-MiniLM-L6-v2".into(),
285 };
286 let result = run_index(&args, OutputFormat::Compact);
287 assert!(result.is_ok(), "index --files failed: {:?}", result.err());
288 }
289}