Skip to main content

git_semantic/cli/
commands.rs

1use anyhow::{Context, Result};
2use indicatif::{ProgressBar, ProgressStyle};
3use std::path::Path;
4use tracing::info;
5
6use crate::embedding::ModelManager;
7use crate::git::{GitError, RepositoryParser};
8use crate::index::{IndexBuilder, IndexError, IndexStorage, SemanticIndex};
9use crate::search::SearchEngine;
10
11use super::SearchFilters;
12
13pub fn init(force: bool) -> Result<()> {
14    println!("šŸš€ Initializing git-semantic...\n");
15
16    let model_manager = ModelManager::new()?;
17
18    if force || !model_manager.is_model_downloaded() {
19        println!("šŸ“„ Downloading embedding model (bge-small-en-v1.5, ~130MB)...");
20        println!("This is a one-time setup and may take a few minutes.\n");
21
22        let pb = ProgressBar::new_spinner();
23        pb.set_style(
24            ProgressStyle::default_spinner()
25                .template("{spinner:.green} {msg}")
26                .unwrap(),
27        );
28        pb.set_message("Downloading model...");
29
30        model_manager.download_model()?;
31
32        pb.finish_with_message("āœ… Model downloaded successfully!");
33    } else {
34        println!("āœ… Model already downloaded");
35    }
36
37    println!("\nšŸŽ‰ git-semantic is ready to use!");
38    println!("\nNext steps:");
39    println!("  1. Navigate to a git repository");
40    println!("  2. Run: git-semantic index");
41    println!("  3. Run: git-semantic search \"your query\"");
42
43    Ok(())
44}
45
46pub fn index(repo_path: &str, include_diffs: bool, force: bool) -> Result<()> {
47    let path = Path::new(repo_path);
48    let storage = IndexStorage::new(path)?;
49
50    let existing_index = if force {
51        None
52    } else {
53        match storage.load() {
54            Ok(idx) => Some(idx),
55            Err(IndexError::IndexNotFound) => None,
56            Err(e) => return Err(e).context("Failed to load existing index"),
57        }
58    };
59
60    match existing_index {
61        Some(existing) => {
62            let existing_mode = existing.metadata.include_diffs;
63
64            if existing_mode != include_diffs {
65                if !include_diffs && existing_mode {
66                    // Full index already exists, quick is a subset — just do incremental with full mode
67                    println!(
68                        "ā„¹ļø  Index was built in full mode (with diffs), which is a superset of quick mode.\n\
69                         Keeping full mode and checking for new commits.\n"
70                    );
71                    incremental_index(path, &storage, existing, existing_mode)?;
72                } else {
73                    // Quick index exists, full requested — requires re-embedding everything
74                    println!(
75                        "āš ļø  Index was built in quick mode (messages only). Switching to full mode \
76                         (with diffs) requires re-embedding all {} commits.\n\
77                         Run with --force to rebuild the index.",
78                        existing.entries.len()
79                    );
80                }
81                return Ok(());
82            }
83
84            incremental_index(path, &storage, existing, include_diffs)?;
85        }
86        None => {
87            full_index(path, &storage, include_diffs)?;
88        }
89    }
90
91    Ok(())
92}
93
94fn full_index(path: &Path, storage: &IndexStorage, include_diffs: bool) -> Result<()> {
95    let mode = if include_diffs { "full" } else { "quick" };
96    println!(
97        "šŸ“š Indexing repository ({} mode): {}\n",
98        mode,
99        path.display()
100    );
101
102    info!("Parsing git repository...");
103    let parser = RepositoryParser::new(path)?;
104    let commits = parser.parse_commits(include_diffs)?;
105
106    println!("Found {} commits to index\n", commits.len());
107
108    let model_manager = ModelManager::new()?;
109    let mut builder = IndexBuilder::new(model_manager, include_diffs)?;
110
111    // Commits are in newest-first order from revwalk; track HEAD as last_commit
112    if let Some(first) = commits.first() {
113        builder.set_last_commit(first.hash.clone());
114    }
115
116    let pb = make_progress_bar(commits.len() as u64);
117
118    for commit in commits {
119        builder.add_commit(commit)?;
120        pb.inc(1);
121    }
122
123    pb.finish_with_message("āœ… Commits indexed");
124
125    println!("\nšŸ’¾ Saving index...");
126    let index = builder.build();
127    storage.save(&index)?;
128
129    print_index_stats(&index, storage)?;
130
131    Ok(())
132}
133
134fn incremental_index(
135    path: &Path,
136    storage: &IndexStorage,
137    existing: SemanticIndex,
138    include_diffs: bool,
139) -> Result<()> {
140    let parser = RepositoryParser::new(path)?;
141
142    let new_commits = match parser.parse_commits_since(&existing.last_commit, include_diffs) {
143        Ok(commits) => commits,
144        Err(GitError::CommitNotFound(_)) => {
145            println!(
146                "āš ļø  Previously indexed commit {} not found in history (was the branch rebased?).",
147                &existing.last_commit[..7.min(existing.last_commit.len())]
148            );
149            println!("Re-indexing from scratch...\n");
150            return full_index(path, storage, include_diffs);
151        }
152        Err(err) => {
153            return Err(err.into());
154        }
155    };
156
157    if new_commits.is_empty() {
158        println!(
159            "āœ… Index is already up to date! ({} commits indexed)",
160            existing.entries.len()
161        );
162        return Ok(());
163    }
164
165    let mode = if include_diffs { "full" } else { "quick" };
166    println!(
167        "šŸ“š Updating index ({} mode): {} ({} new commits)\n",
168        mode,
169        path.display(),
170        new_commits.len()
171    );
172
173    let model_manager = ModelManager::new()?;
174    let mut builder = IndexBuilder::from_existing(existing, model_manager)?;
175
176    // New commits are newest-first; update last_commit to the newest
177    if let Some(first) = new_commits.first() {
178        builder.set_last_commit(first.hash.clone());
179    }
180
181    let pb = make_progress_bar(new_commits.len() as u64);
182
183    for commit in new_commits {
184        builder.add_commit(commit)?;
185        pb.inc(1);
186    }
187
188    pb.finish_with_message("āœ… New commits indexed");
189
190    println!("\nšŸ’¾ Saving index...");
191    let index = builder.build();
192    storage.save(&index)?;
193
194    print_index_stats(&index, storage)?;
195
196    Ok(())
197}
198
199pub fn update(repo_path: &str) -> Result<()> {
200    println!(
201        "Note: `git-semantic index` now automatically handles incremental updates.\n\
202         The `update` command will be removed in a future release.\n"
203    );
204    index(repo_path, true, false)
205}
206
207pub fn search(
208    repo_path: &str,
209    query: &str,
210    num_results: usize,
211    filters: SearchFilters,
212) -> Result<()> {
213    let path = Path::new(repo_path);
214
215    let storage = IndexStorage::new(path)?;
216    let index = storage.load()?;
217
218    let model_manager = ModelManager::new()?;
219    let mut engine = SearchEngine::new(model_manager)?;
220    let results = engine.search(&index, query, num_results, filters)?;
221
222    if results.is_empty() {
223        println!("No results found for: \"{}\"", query);
224        return Ok(());
225    }
226
227    println!("šŸŽÆ Most Relevant Commits for: \"{}\"\n", query);
228
229    for result in results {
230        println!(
231            "{}. {} - {} ({:.2} similarity)",
232            result.rank,
233            &result.commit.hash[..7],
234            result.commit.message.lines().next().unwrap_or(""),
235            result.similarity
236        );
237        println!(
238            "   Author: {}, {}",
239            result.commit.author, result.commit.date
240        );
241
242        if !result.commit.diff_summary.is_empty() {
243            let preview: String = result
244                .commit
245                .diff_summary
246                .lines()
247                .take(2)
248                .collect::<Vec<_>>()
249                .join("\n   ");
250            if !preview.is_empty() {
251                println!("   {}", preview);
252            }
253        }
254
255        println!();
256    }
257
258    Ok(())
259}
260
261pub fn stats(repo_path: &str) -> Result<()> {
262    let path = Path::new(repo_path);
263
264    let storage = IndexStorage::new(path)?;
265    let index = storage.load()?;
266
267    println!("šŸ“Š Index Statistics\n");
268    println!("Repository: {}", path.display());
269    println!("Total commits indexed: {}", index.entries.len());
270    println!("Model version: {}", index.model_version);
271    println!("Last indexed commit: {}", index.last_commit);
272    println!(
273        "Index mode: {}",
274        if index.metadata.include_diffs {
275            "full (with diffs)"
276        } else {
277            "quick (messages only)"
278        }
279    );
280    println!("Index size: ~{:.2} MB", storage.index_size_mb()?);
281    println!(
282        "Created: {}",
283        index.metadata.created_at.format("%Y-%m-%d %H:%M:%S")
284    );
285    println!(
286        "Last updated: {}",
287        index.metadata.updated_at.format("%Y-%m-%d %H:%M:%S")
288    );
289
290    Ok(())
291}
292
293fn make_progress_bar(total: u64) -> ProgressBar {
294    let pb = ProgressBar::new(total);
295    pb.set_style(
296        ProgressStyle::default_bar()
297            .template("[{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} {msg}")
298            .unwrap()
299            .progress_chars("=>-"),
300    );
301    pb
302}
303
304fn print_index_stats(index: &SemanticIndex, storage: &IndexStorage) -> Result<()> {
305    println!("āœ… Index saved successfully!");
306    println!("\nšŸ“Š Index statistics:");
307    println!("  - Total commits: {}", index.entries.len());
308    println!(
309        "  - Mode: {}",
310        if index.metadata.include_diffs {
311            "full (with diffs)"
312        } else {
313            "quick (messages only)"
314        }
315    );
316    println!("  - Model: {}", index.model_version);
317    println!("  - Index size: ~{:.2} MB", storage.index_size_mb()?);
318    Ok(())
319}