Skip to main content

the_code_graph_cli/commands/
index.rs

1use 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    /// Create a git Command that is isolated from any parent repo context.
102    /// Clears GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE to prevent lefthook
103    /// env vars from leaking into test-created repos.
104    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        // Full index first
169        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        // Commit the file so git status shows it as clean
179        git(root).args(["add", "."]).output().unwrap();
180        git(root)
181            .args(["commit", "-m", "initial"])
182            .output()
183            .unwrap();
184
185        // Modify file
186        fs::write(
187            src.join("main.ts"),
188            "export function hello(): void {}\nexport function world(): void {}",
189        )
190        .unwrap();
191
192        // Incremental index
193        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        // Full index first
219        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        // Commit everything
229        git(root).args(["add", "."]).output().unwrap();
230        git(root)
231            .args(["commit", "-m", "initial"])
232            .output()
233            .unwrap();
234
235        // Incremental with no changes
236        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        // Full index first
262        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        // Modify file
272        fs::write(
273            src.join("main.ts"),
274            "export function hello(): void {}\nexport function bar(): void {}",
275        )
276        .unwrap();
277
278        // Index specific files
279        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}