1use crate::core::{find_repo_root, list_refs, Object, ObjectHash};
2use crate::response::{VerifyResponse, VerifyResult};
3use crate::storage::ObjectStore;
4
5pub fn execute() -> Result<VerifyResponse, crate::errors::LitError> {
6 let repo_root = find_repo_root()?;
7 let store = ObjectStore::new(&repo_root);
8
9 let mut checks = Vec::new();
10 let mut objects_checked = 0usize;
11 let mut refs_checked = 0usize;
12 let mut all_valid = true;
13
14 match verify_objects(&repo_root, &store) {
16 Ok((count, results)) => {
17 objects_checked = count;
18 for r in results {
19 if r.status != "ok" {
20 all_valid = false;
21 }
22 checks.push(r);
23 }
24 }
25 Err(e) => {
26 all_valid = false;
27 checks.push(VerifyResult {
28 check: "object_store".to_string(),
29 status: "error".to_string(),
30 details: Some(e),
31 });
32 }
33 }
34
35 match verify_refs(&repo_root, &store) {
37 Ok((count, results)) => {
38 refs_checked = count;
39 for r in results {
40 if r.status != "ok" {
41 all_valid = false;
42 }
43 checks.push(r);
44 }
45 }
46 Err(e) => {
47 all_valid = false;
48 checks.push(VerifyResult {
49 check: "refs".to_string(),
50 status: "error".to_string(),
51 details: Some(e),
52 });
53 }
54 }
55
56 match verify_dag(&repo_root, &store) {
58 Ok(result) => {
59 if result.status != "ok" {
60 all_valid = false;
61 }
62 checks.push(result);
63 }
64 Err(e) => {
65 all_valid = false;
66 checks.push(VerifyResult {
67 check: "dag_connectivity".to_string(),
68 status: "error".to_string(),
69 details: Some(e.internal_message().to_string()),
70 });
71 }
72 }
73
74 match verify_index(&repo_root, &store) {
76 Ok(result) => {
77 if result.status != "ok" {
78 all_valid = false;
79 }
80 checks.push(result);
81 }
82 Err(e) => {
83 all_valid = false;
84 checks.push(VerifyResult {
85 check: "index".to_string(),
86 status: "error".to_string(),
87 details: Some(e.internal_message().to_string()),
88 });
89 }
90 }
91
92 let message = if all_valid {
93 "Repository is valid".to_string()
94 } else {
95 "Repository has errors".to_string()
96 };
97
98 Ok(VerifyResponse {
99 valid: all_valid,
100 checks,
101 objects_checked,
102 refs_checked,
103 message,
104 })
105}
106
107fn verify_objects(
108 repo_root: &std::path::Path,
109 store: &ObjectStore,
110) -> Result<(usize, Vec<VerifyResult>), String> {
111 let objects_dir = repo_root.join(".lit").join("objects");
112 let mut results = Vec::new();
113 let mut count = 0usize;
114 let mut corrupt = 0usize;
115
116 if !objects_dir.exists() {
117 return Ok((
118 0,
119 vec![VerifyResult {
120 check: "object_store".to_string(),
121 status: "ok".to_string(),
122 details: Some("No objects directory (empty repository)".to_string()),
123 }],
124 ));
125 }
126
127 for dir_entry in
129 std::fs::read_dir(&objects_dir).map_err(|e| format!("Failed to read objects dir: {}", e))?
130 {
131 let dir_entry = dir_entry.map_err(|e| format!("Dir entry error: {}", e))?;
132 if !dir_entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
133 continue;
134 }
135
136 let prefix = dir_entry.file_name().to_string_lossy().to_string();
137 let subdir = objects_dir.join(&prefix);
138
139 for file_entry in std::fs::read_dir(&subdir)
140 .map_err(|e| format!("Failed to read subdir {}: {}", prefix, e))?
141 {
142 let file_entry = file_entry.map_err(|e| format!("File entry error: {}", e))?;
143 let filename = file_entry.file_name().to_string_lossy().to_string();
144 let hash_str = format!("{}{}", prefix, filename);
145
146 let hash = ObjectHash::from_hex(hash_str.clone());
147 match store.read(&hash) {
148 Ok(_) => count += 1,
149 Err(e) => {
150 corrupt += 1;
151 count += 1;
152 results.push(VerifyResult {
153 check: format!("object:{}", &hash_str[..16.min(hash_str.len())]),
154 status: "error".to_string(),
155 details: Some(format!("Corrupt object: {}", e)),
156 });
157 }
158 }
159 }
160 }
161
162 if corrupt == 0 {
163 results.push(VerifyResult {
164 check: "object_hashes".to_string(),
165 status: "ok".to_string(),
166 details: Some(format!("All {} objects verified", count)),
167 });
168 }
169
170 Ok((count, results))
171}
172
173fn verify_refs(
174 repo_root: &std::path::Path,
175 store: &ObjectStore,
176) -> Result<(usize, Vec<VerifyResult>), String> {
177 let mut results = Vec::new();
178 let mut count = 0usize;
179 let mut dangling = 0usize;
180
181 let heads = list_refs(repo_root, "heads").unwrap_or_default();
183 for r in &heads {
184 let (name, hash) = (&r.name, &r.hash);
185 count += 1;
186 let hash_obj = ObjectHash::from_hex(hash.clone());
187 match store.read(&hash_obj) {
188 Ok(Object::Commit(_)) => {}
189 Ok(_) => {
190 dangling += 1;
191 results.push(VerifyResult {
192 check: format!("ref:heads/{}", name),
193 status: "error".to_string(),
194 details: Some("Points to non-commit object".to_string()),
195 });
196 }
197 Err(_) => {
198 dangling += 1;
199 results.push(VerifyResult {
200 check: format!("ref:heads/{}", name),
201 status: "error".to_string(),
202 details: Some(format!(
203 "Dangling reference: {}",
204 &hash[..16.min(hash.len())]
205 )),
206 });
207 }
208 }
209 }
210
211 let tags = list_refs(repo_root, "tags").unwrap_or_default();
213 for r in &tags {
214 let (name, hash) = (&r.name, &r.hash);
215 count += 1;
216 let hash_obj = ObjectHash::from_hex(hash.clone());
217 if store.read(&hash_obj).is_err() {
218 dangling += 1;
219 results.push(VerifyResult {
220 check: format!("ref:tags/{}", name),
221 status: "error".to_string(),
222 details: Some(format!(
223 "Dangling reference: {}",
224 &hash[..16.min(hash.len())]
225 )),
226 });
227 }
228 }
229
230 if dangling == 0 {
231 results.push(VerifyResult {
232 check: "refs".to_string(),
233 status: "ok".to_string(),
234 details: Some(format!("All {} refs valid", count)),
235 });
236 }
237
238 Ok((count, results))
239}
240
241fn verify_dag(
242 repo_root: &std::path::Path,
243 store: &ObjectStore,
244) -> Result<VerifyResult, crate::errors::LitError> {
245 let heads = list_refs(repo_root, "heads").unwrap_or_default();
246
247 let mut visited = std::collections::HashSet::new();
248 let mut missing_parents = Vec::new();
249
250 for r in &heads {
251 let hash = &r.hash;
252 walk_commit_dag(store, hash, &mut visited, &mut missing_parents);
253 }
254
255 if missing_parents.is_empty() {
256 Ok(VerifyResult {
257 check: "dag_connectivity".to_string(),
258 status: "ok".to_string(),
259 details: Some(format!(
260 "DAG is connected ({} commits reachable)",
261 visited.len()
262 )),
263 })
264 } else {
265 Ok(VerifyResult {
266 check: "dag_connectivity".to_string(),
267 status: "error".to_string(),
268 details: Some(format!(
269 "{} missing parent commit(s)",
270 missing_parents.len()
271 )),
272 })
273 }
274}
275
276fn walk_commit_dag(
277 store: &ObjectStore,
278 hash: &str,
279 visited: &mut std::collections::HashSet<String>,
280 missing: &mut Vec<String>,
281) {
282 if visited.contains(hash) {
283 return;
284 }
285 visited.insert(hash.to_string());
286
287 let hash_obj = ObjectHash::from_hex(hash.to_string());
288 if let Ok(Object::Commit(commit)) = store.read(&hash_obj) {
289 for parent in &commit.parents {
290 let parent_str = parent.to_string();
291 if store.exists(parent) {
292 walk_commit_dag(store, &parent_str, visited, missing);
293 } else {
294 missing.push(parent_str);
295 }
296 }
297 }
298}
299
300fn verify_index(
301 repo_root: &std::path::Path,
302 store: &ObjectStore,
303) -> Result<VerifyResult, crate::errors::LitError> {
304 let index = crate::storage::Index::load(repo_root)?;
305 let mut missing = 0usize;
306
307 for entry in index.sorted_entries() {
308 let hash = ObjectHash::from_hex(entry.hash.clone());
309 if !store.exists(&hash) {
310 missing += 1;
311 }
312 }
313
314 if missing == 0 {
315 Ok(VerifyResult {
316 check: "index".to_string(),
317 status: "ok".to_string(),
318 details: Some(format!(
319 "All {} index entries reference valid objects",
320 index.entries.len()
321 )),
322 })
323 } else {
324 Ok(VerifyResult {
325 check: "index".to_string(),
326 status: "error".to_string(),
327 details: Some(format!(
328 "{} index entries reference missing objects",
329 missing
330 )),
331 })
332 }
333}