1use crate::core::{find_repo_root, read_head, Object, ObjectHash};
2use crate::response::{SearchMatch, SearchResponse};
3use crate::storage::ObjectStore;
4use std::fs;
5use std::path::Path;
6use walkdir::WalkDir;
7
8pub fn execute(
9 query: String,
10 messages: bool,
11 metadata_filter: Option<String>,
12 max_results: usize,
13) -> Result<SearchResponse, crate::errors::LitError> {
14 let repo_root = find_repo_root()?;
15 let store = ObjectStore::new(&repo_root);
16
17 if messages {
18 return search_commit_messages(&repo_root, &store, &query, max_results);
19 }
20
21 if let Some(filter) = metadata_filter {
22 return search_metadata(&repo_root, &store, &filter, max_results);
23 }
24
25 search_file_contents(&repo_root, &query, max_results)
27}
28
29fn search_file_contents(
30 repo_root: &Path,
31 query: &str,
32 max_results: usize,
33) -> Result<SearchResponse, crate::errors::LitError> {
34 let query_lower = query.to_lowercase();
35 let mut matches = Vec::new();
36
37 for entry in WalkDir::new(repo_root).into_iter().filter_entry(|e| {
38 let name = e.file_name().to_string_lossy();
39 !name.starts_with('.') && name != "target" && name != "node_modules"
40 }) {
41 let entry = entry.map_err(|e| format!("Walk error: {}", e))?;
42 if !entry.file_type().is_file() {
43 continue;
44 }
45 let path = entry.path();
46 if path.starts_with(repo_root.join(".lit")) {
47 continue;
48 }
49
50 let content = match fs::read(path) {
52 Ok(c) => c,
53 Err(_) => continue,
54 };
55 if content.iter().take(512).any(|&b| b == 0) {
56 continue;
57 }
58 let text = match String::from_utf8(content) {
59 Ok(t) => t,
60 Err(_) => continue,
61 };
62
63 let rel_path = path
64 .strip_prefix(repo_root)
65 .unwrap_or(path)
66 .to_string_lossy()
67 .replace('\\', "/");
68
69 for (line_num, line) in text.lines().enumerate() {
70 if line.to_lowercase().contains(&query_lower) {
71 matches.push(SearchMatch {
72 file: rel_path.clone(),
73 line_number: line_num + 1,
74 content: line.to_string(),
75 commit: None,
76 match_type: "content".to_string(),
77 });
78 if matches.len() >= max_results {
79 break;
80 }
81 }
82 }
83 if matches.len() >= max_results {
84 break;
85 }
86 }
87
88 let total = matches.len();
89 Ok(SearchResponse {
90 query: query.to_string(),
91 match_type: "content".to_string(),
92 matches,
93 total,
94 })
95}
96
97fn search_commit_messages(
98 repo_root: &Path,
99 store: &ObjectStore,
100 query: &str,
101 max_results: usize,
102) -> Result<SearchResponse, crate::errors::LitError> {
103 let query_lower = query.to_lowercase();
104 let mut matches = Vec::new();
105
106 let head = match read_head(repo_root) {
107 Ok(h) => h,
108 Err(_) => {
109 return Ok(SearchResponse {
110 query: query.to_string(),
111 match_type: "message".to_string(),
112 matches: Vec::new(),
113 total: 0,
114 })
115 }
116 };
117
118 let mut current = Some(head);
119 while let Some(hash) = current {
120 if matches.len() >= max_results {
121 break;
122 }
123 let hash_obj = ObjectHash::from_hex(hash.clone());
124 match store.read(&hash_obj) {
125 Ok(Object::Commit(commit)) => {
126 if commit.message.to_lowercase().contains(&query_lower) {
127 for (i, line) in commit.message.lines().enumerate() {
128 if line.to_lowercase().contains(&query_lower) {
129 matches.push(SearchMatch {
130 file: String::new(),
131 line_number: i + 1,
132 content: line.to_string(),
133 commit: Some(hash.clone()),
134 match_type: "message".to_string(),
135 });
136 }
137 }
138 }
139 current = commit.parents.first().map(|p| p.to_string());
140 }
141 _ => break,
142 }
143 }
144
145 let total = matches.len();
146 Ok(SearchResponse {
147 query: query.to_string(),
148 match_type: "message".to_string(),
149 matches,
150 total,
151 })
152}
153
154fn search_metadata(
155 repo_root: &Path,
156 store: &ObjectStore,
157 filter: &str,
158 max_results: usize,
159) -> Result<SearchResponse, crate::errors::LitError> {
160 let (key, value) = filter
162 .split_once('=')
163 .ok_or("Metadata filter must be in key=value format")?;
164
165 let mut matches = Vec::new();
166
167 let head = match read_head(repo_root) {
168 Ok(h) => h,
169 Err(_) => {
170 return Ok(SearchResponse {
171 query: filter.to_string(),
172 match_type: "metadata".to_string(),
173 matches: Vec::new(),
174 total: 0,
175 })
176 }
177 };
178
179 let mut current = Some(head);
180 while let Some(hash) = current {
181 if matches.len() >= max_results {
182 break;
183 }
184 let hash_obj = ObjectHash::from_hex(hash.clone());
185 match store.read(&hash_obj) {
186 Ok(Object::Commit(commit)) => {
187 if let Some(meta) = &commit.metadata {
188 if let Some(meta_value) = meta.get(key) {
189 let meta_str = match meta_value {
190 serde_json::Value::String(s) => s.clone(),
191 other => other.to_string(),
192 };
193 if meta_str.to_lowercase() == value.to_lowercase() {
194 matches.push(SearchMatch {
195 file: String::new(),
196 line_number: 0,
197 content: format!("{}: {}", commit.message, meta),
198 commit: Some(hash.clone()),
199 match_type: "metadata".to_string(),
200 });
201 }
202 }
203 }
204 current = commit.parents.first().map(|p| p.to_string());
205 }
206 _ => break,
207 }
208 }
209
210 let total = matches.len();
211 Ok(SearchResponse {
212 query: filter.to_string(),
213 match_type: "metadata".to_string(),
214 matches,
215 total,
216 })
217}