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 = 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 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 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 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 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}