Skip to main content

gobby_code/commands/
index.rs

1use crate::config;
2use crate::config::Context;
3use crate::index::api::{self, IndexOutcome, IndexRequest};
4use crate::output::{self, Format};
5use crate::projection::sync::{self, ProjectionSyncReports};
6use crate::utils::short_id;
7use serde::Serialize;
8
9pub fn run(
10    ctx: &Context,
11    path: Option<String>,
12    files: Option<Vec<String>>,
13    full: bool,
14    require_cpp_semantics: bool,
15    sync_projections: bool,
16    format: Format,
17) -> anyhow::Result<()> {
18    let (target_ctx, path_filter) = resolve_index_context(ctx, path.as_deref())?;
19    let explicit_files: Vec<std::path::PathBuf> = files
20        .unwrap_or_default()
21        .into_iter()
22        .map(std::path::PathBuf::from)
23        .collect();
24    let request = IndexRequest {
25        project_root: target_ctx.project_root.clone(),
26        path_filter: if explicit_files.is_empty() {
27            path_filter
28        } else {
29            None
30        },
31        explicit_files,
32        full,
33        require_cpp_semantics,
34        sync_projections,
35    };
36
37    let outcome = api::index_files(request, &target_ctx)?;
38    if sync_projections {
39        let projections = sync::sync_after_index(&target_ctx, &outcome.indexed_file_paths)?;
40        let payload = sync_projections_payload(&outcome, projections);
41        return match format {
42            Format::Json => output::print_json(&payload),
43            Format::Text => output::print_text(&sync_projections_text(&payload)?),
44        };
45    }
46
47    match format {
48        Format::Json => output::print_json(&outcome),
49        Format::Text => output::print_text(&format!(
50            "Indexed {} files ({} skipped), {} symbols, {} chunks in {}ms",
51            outcome.indexed_files,
52            outcome.skipped_files,
53            outcome.symbols_indexed,
54            outcome.chunks_indexed,
55            outcome.durations.total_ms
56        )),
57    }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
61pub(crate) struct IndexSyncProjectionsOutput {
62    pub indexed_files: usize,
63    pub skipped_files: usize,
64    pub symbols_indexed: usize,
65    pub chunks_indexed: usize,
66    pub projections: ProjectionSyncReports,
67}
68
69pub(crate) fn sync_projections_payload(
70    outcome: &IndexOutcome,
71    projections: ProjectionSyncReports,
72) -> IndexSyncProjectionsOutput {
73    IndexSyncProjectionsOutput {
74        indexed_files: outcome.indexed_files,
75        skipped_files: outcome.skipped_files,
76        symbols_indexed: outcome.symbols_indexed,
77        chunks_indexed: outcome.chunks_indexed,
78        projections,
79    }
80}
81
82pub(crate) fn sync_projections_text(
83    payload: &IndexSyncProjectionsOutput,
84) -> anyhow::Result<String> {
85    Ok(serde_json::to_string(payload)?)
86}
87
88fn resolve_index_context(
89    ctx: &Context,
90    path: Option<&str>,
91) -> anyhow::Result<(Context, Option<std::path::PathBuf>)> {
92    let Some(p) = path else {
93        return Ok((
94            clone_context(ctx, ctx.project_root.clone(), ctx.project_id.clone()),
95            None,
96        ));
97    };
98
99    // Resolve root and project_id. If the path belongs to a different project
100    // than the CWD-derived context, re-resolve identity for that project.
101    let target = std::path::PathBuf::from(p);
102    let target_root = crate::config::detect_project_root_from(&target)?;
103    let target_filter = path_filter_for(&target_root, &target);
104    if target_root != ctx.project_root {
105        let identity = crate::config::resolve_project_identity(
106            &target_root,
107            crate::config::MissingIdentity::Generate,
108        )?;
109        crate::config::warn_project_identity(&identity, ctx.quiet);
110        if !ctx.quiet {
111            eprintln!(
112                "Warning: path '{}' belongs to project {} (not {}), re-resolving context",
113                p,
114                short_id(&identity.project_id),
115                &ctx.project_id[..8]
116            );
117        }
118        if identity.should_write_gcode_json {
119            crate::project::ensure_gcode_json(&target_root)?;
120        }
121        Ok((
122            clone_context(ctx, target_root, identity.project_id),
123            target_filter,
124        ))
125    } else {
126        Ok((
127            clone_context(ctx, target_root, ctx.project_id.clone()),
128            target_filter,
129        ))
130    }
131}
132
133fn clone_context(ctx: &Context, project_root: std::path::PathBuf, project_id: String) -> Context {
134    config::Context {
135        database_url: ctx.database_url.clone(),
136        project_root,
137        project_id,
138        quiet: ctx.quiet,
139        falkordb: ctx.falkordb.clone(),
140        qdrant: ctx.qdrant.clone(),
141        embedding: ctx.embedding.clone(),
142        code_vectors: ctx.code_vectors.clone(),
143        daemon_url: ctx.daemon_url.clone(),
144    }
145}
146
147fn path_filter_for(
148    project_root: &std::path::Path,
149    target: &std::path::Path,
150) -> Option<std::path::PathBuf> {
151    let target_abs = if target.is_absolute() {
152        target.to_path_buf()
153    } else {
154        std::env::current_dir()
155            .map(|cwd| cwd.join(target))
156            .unwrap_or_else(|_| project_root.join(target))
157    };
158
159    let root_abs = project_root
160        .canonicalize()
161        .unwrap_or_else(|_| project_root.to_path_buf());
162    let target_abs = target_abs.canonicalize().unwrap_or(target_abs);
163
164    if target_abs == root_abs {
165        None
166    } else {
167        Some(target_abs)
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use crate::index::api::IndexOutcome;
175    use crate::projection::sync::{
176        ProjectionStatus, ProjectionSyncError, ProjectionSyncReport, ProjectionSyncReports,
177    };
178    use serde_json::{Value, json};
179
180    fn sample_outcome() -> IndexOutcome {
181        IndexOutcome {
182            indexed_files: 12,
183            skipped_files: 0,
184            symbols_indexed: 348,
185            chunks_indexed: 921,
186            ..IndexOutcome::default()
187        }
188    }
189
190    fn sample_reports() -> ProjectionSyncReports {
191        ProjectionSyncReports {
192            graph: ProjectionSyncReport {
193                status: ProjectionStatus::Ok,
194                synced_files: 12,
195                synced_symbols: 348,
196                degraded: false,
197                error: None,
198            },
199            vector: ProjectionSyncReport {
200                status: ProjectionStatus::Degraded,
201                synced_files: 0,
202                synced_symbols: 0,
203                degraded: true,
204                error: Some(ProjectionSyncError {
205                    kind: "missing_qdrant_config".to_string(),
206                    message: "Qdrant config is required".to_string(),
207                }),
208            },
209        }
210    }
211
212    #[test]
213    fn sync_projections_json_contract() {
214        let payload = sync_projections_payload(&sample_outcome(), sample_reports());
215        assert_eq!(
216            serde_json::to_value(&payload).expect("payload serializes"),
217            json!({
218                "indexed_files": 12,
219                "skipped_files": 0,
220                "symbols_indexed": 348,
221                "chunks_indexed": 921,
222                "projections": {
223                    "graph": {
224                        "status": "ok",
225                        "synced_files": 12,
226                        "synced_symbols": 348,
227                        "degraded": false,
228                        "error": null
229                    },
230                    "vector": {
231                        "status": "degraded",
232                        "synced_files": 0,
233                        "synced_symbols": 0,
234                        "degraded": true,
235                        "error": {
236                            "kind": "missing_qdrant_config",
237                            "message": "Qdrant config is required"
238                        }
239                    }
240                }
241            })
242        );
243    }
244
245    #[test]
246    fn sync_projections_text_contract() {
247        let payload = sync_projections_payload(&sample_outcome(), sample_reports());
248        let text = sync_projections_text(&payload).expect("text payload");
249        let parsed: Value = serde_json::from_str(&text).expect("text mode is structured JSON");
250        assert_eq!(parsed["indexed_files"], 12);
251        assert_eq!(parsed["projections"]["graph"]["status"], "ok");
252        assert_eq!(parsed["projections"]["vector"]["status"], "degraded");
253    }
254}