Skip to main content

lit/commands/
verify.rs

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    // 1. Verify all objects
15    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    // 2. Verify refs
36    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    // 3. Verify DAG connectivity
57    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    // 4. Verify index consistency
75    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    // Walk the objects directory
128    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    // Check heads
182    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    // Check tags
212    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}