git_semantic/cli/
commands.rs1use 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 = match storage.load() {
51 Ok(idx) => Some(idx),
52 Err(IndexError::IndexNotFound) => None,
53 Err(e) => return Err(e).context("Failed to load existing index"),
54 };
55
56 match existing_index {
57 Some(existing) => {
58 let existing_mode = existing.metadata.include_diffs;
59
60 if force {
61 if existing_mode && !include_diffs {
63 println!(
64 "ā ļø Downgrading from full mode to quick mode. This will discard \
65 diff embeddings for {} commits.\n\
66 Switching back to full mode later will require re-embedding all commits.\n",
67 existing.entries.len()
68 );
69 }
70 return full_index(path, &storage, include_diffs);
71 }
72
73 if existing_mode != include_diffs {
74 if !include_diffs && existing_mode {
75 println!(
77 "ā¹ļø Index was built in full mode (with diffs), which is a superset of quick mode.\n\
78 Keeping full mode and checking for new commits.\n\
79 To downgrade to quick mode (smaller index), run with --force.\n\
80 Note: switching back to full mode later will require re-embedding all commits.\n"
81 );
82 incremental_index(path, &storage, existing, existing_mode)?;
83 } else {
84 println!(
86 "ā ļø Index was built in quick mode (messages only). Switching to full mode \
87 (with diffs) requires re-embedding all {} commits.\n\
88 Run with --force to rebuild the index.",
89 existing.entries.len()
90 );
91 }
92 return Ok(());
93 }
94
95 incremental_index(path, &storage, existing, include_diffs)?;
96 }
97 None => {
98 full_index(path, &storage, include_diffs)?;
99 }
100 }
101
102 Ok(())
103}
104
105fn full_index(path: &Path, storage: &IndexStorage, include_diffs: bool) -> Result<()> {
106 let mode = if include_diffs { "full" } else { "quick" };
107 println!(
108 "š Indexing repository ({} mode): {}\n",
109 mode,
110 path.display()
111 );
112
113 info!("Parsing git repository...");
114 let parser = RepositoryParser::new(path)?;
115 let commits = parser.parse_commits(include_diffs)?;
116
117 println!("Found {} commits to index\n", commits.len());
118
119 let model_manager = ModelManager::new()?;
120 let mut builder = IndexBuilder::new(model_manager, include_diffs)?;
121
122 if let Some(first) = commits.first() {
124 builder.set_last_commit(first.hash.clone());
125 }
126
127 let pb = make_progress_bar(commits.len() as u64);
128
129 for commit in commits {
130 builder.add_commit(commit)?;
131 pb.inc(1);
132 }
133
134 pb.finish_with_message("ā
Commits indexed");
135
136 println!("\nš¾ Saving index...");
137 let index = builder.build();
138 storage.save(&index)?;
139
140 print_index_stats(&index, storage)?;
141
142 Ok(())
143}
144
145fn incremental_index(
146 path: &Path,
147 storage: &IndexStorage,
148 existing: SemanticIndex,
149 include_diffs: bool,
150) -> Result<()> {
151 let parser = RepositoryParser::new(path)?;
152
153 let new_commits = match parser.parse_commits_since(&existing.last_commit, include_diffs) {
154 Ok(commits) => commits,
155 Err(GitError::CommitNotFound(_)) => {
156 println!(
157 "ā ļø Previously indexed commit {} not found in history (was the branch rebased?).",
158 &existing.last_commit[..7.min(existing.last_commit.len())]
159 );
160 println!("Re-indexing from scratch...\n");
161 return full_index(path, storage, include_diffs);
162 }
163 Err(err) => {
164 return Err(err.into());
165 }
166 };
167
168 if new_commits.is_empty() {
169 println!(
170 "ā
Index is already up to date! ({} commits indexed)",
171 existing.entries.len()
172 );
173 return Ok(());
174 }
175
176 let mode = if include_diffs { "full" } else { "quick" };
177 println!(
178 "š Updating index ({} mode): {} ({} new commits)\n",
179 mode,
180 path.display(),
181 new_commits.len()
182 );
183
184 let model_manager = ModelManager::new()?;
185 let mut builder = IndexBuilder::from_existing(existing, model_manager)?;
186
187 if let Some(first) = new_commits.first() {
189 builder.set_last_commit(first.hash.clone());
190 }
191
192 let pb = make_progress_bar(new_commits.len() as u64);
193
194 for commit in new_commits {
195 builder.add_commit(commit)?;
196 pb.inc(1);
197 }
198
199 pb.finish_with_message("ā
New commits indexed");
200
201 println!("\nš¾ Saving index...");
202 let index = builder.build();
203 storage.save(&index)?;
204
205 print_index_stats(&index, storage)?;
206
207 Ok(())
208}
209
210pub fn update(repo_path: &str) -> Result<()> {
211 println!(
212 "Note: `git-semantic index` now automatically handles incremental updates.\n\
213 The `update` command will be removed in a future release.\n"
214 );
215 index(repo_path, true, false)
216}
217
218pub fn search(
219 repo_path: &str,
220 query: &str,
221 num_results: usize,
222 filters: SearchFilters,
223) -> Result<()> {
224 let path = Path::new(repo_path);
225
226 let storage = IndexStorage::new(path)?;
227 let index = storage.load()?;
228
229 let model_manager = ModelManager::new()?;
230 let mut engine = SearchEngine::new(model_manager)?;
231 let results = engine.search(&index, query, num_results, filters)?;
232
233 if results.is_empty() {
234 println!("No results found for: \"{}\"", query);
235 return Ok(());
236 }
237
238 println!("šÆ Most Relevant Commits for: \"{}\"\n", query);
239
240 for result in results {
241 println!(
242 "{}. {} - {} ({:.2} similarity)",
243 result.rank,
244 &result.commit.hash[..7],
245 result.commit.message.lines().next().unwrap_or(""),
246 result.similarity
247 );
248 println!(
249 " Author: {}, {}",
250 result.commit.author, result.commit.date
251 );
252
253 if !result.commit.diff_summary.is_empty() {
254 let preview: String = result
255 .commit
256 .diff_summary
257 .lines()
258 .take(2)
259 .collect::<Vec<_>>()
260 .join("\n ");
261 if !preview.is_empty() {
262 println!(" {}", preview);
263 }
264 }
265
266 println!();
267 }
268
269 Ok(())
270}
271
272pub fn stats(repo_path: &str) -> Result<()> {
273 let path = Path::new(repo_path);
274
275 let storage = IndexStorage::new(path)?;
276 let index = storage.load()?;
277
278 println!("š Index Statistics\n");
279 println!("Repository: {}", path.display());
280 println!("Total commits indexed: {}", index.entries.len());
281 println!("Model version: {}", index.model_version);
282 println!("Last indexed commit: {}", index.last_commit);
283 println!(
284 "Index mode: {}",
285 if index.metadata.include_diffs {
286 "full (with diffs)"
287 } else {
288 "quick (messages only)"
289 }
290 );
291 println!("Index size: ~{:.2} MB", storage.index_size_mb()?);
292 println!(
293 "Created: {}",
294 index.metadata.created_at.format("%Y-%m-%d %H:%M:%S")
295 );
296 println!(
297 "Last updated: {}",
298 index.metadata.updated_at.format("%Y-%m-%d %H:%M:%S")
299 );
300
301 Ok(())
302}
303
304fn make_progress_bar(total: u64) -> ProgressBar {
305 let pb = ProgressBar::new(total);
306 pb.set_style(
307 ProgressStyle::default_bar()
308 .template("[{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} {msg}")
309 .unwrap()
310 .progress_chars("=>-"),
311 );
312 pb
313}
314
315fn print_index_stats(index: &SemanticIndex, storage: &IndexStorage) -> Result<()> {
316 println!("ā
Index saved successfully!");
317 println!("\nš Index statistics:");
318 println!(" - Total commits: {}", index.entries.len());
319 println!(
320 " - Mode: {}",
321 if index.metadata.include_diffs {
322 "full (with diffs)"
323 } else {
324 "quick (messages only)"
325 }
326 );
327 println!(" - Model: {}", index.model_version);
328 println!(" - Index size: ~{:.2} MB", storage.index_size_mb()?);
329 Ok(())
330}