use alloc::collections::BTreeSet;
use alloc::collections::VecDeque;
use alloc::string::String;
use alloc::vec::Vec;
use crate::error::{Error, Result};
use crate::object::tree::FileMode;
use crate::object::{Commit, ObjectType, Tag, Tree};
use crate::odb::ObjectDatabase;
use crate::oid::ObjectId;
use crate::repository::Repository;
#[derive(Debug, Clone)]
pub struct Problem {
pub id: ObjectId,
pub referenced_by: Option<ObjectId>,
pub message: String,
}
#[derive(Debug, Clone, Default)]
pub struct Report {
pub checked: usize,
pub problems: Vec<Problem>,
pub dangling: Vec<ObjectId>,
}
impl Report {
pub fn is_ok(&self) -> bool {
self.problems.is_empty()
}
}
pub fn fsck(repo: &Repository) -> Result<Report> {
let odb = repo.objects();
let mut roots: Vec<ObjectId> = Vec::new();
if let Ok(head) = repo.head_id() {
roots.push(head);
}
for (_name, id) in repo.refs().list()? {
roots.push(id);
}
for entry in &repo.index()?.entries {
roots.push(entry.id);
}
let mut seen = BTreeSet::new();
let mut problems = Vec::new();
let mut checked = 0usize;
let mut queue: VecDeque<(ObjectId, Option<ObjectId>)> =
roots.iter().map(|id| (*id, None)).collect();
while let Some((id, parent)) = queue.pop_front() {
if !seen.insert(id) {
continue;
}
let (ty, payload) = match odb.read(&id) {
Ok(v) => v,
Err(Error::NotFound(_)) => {
problems.push(Problem {
id,
referenced_by: parent,
message: "missing object".into(),
});
continue;
}
Err(e) => {
problems.push(Problem {
id,
referenced_by: parent,
message: alloc::format!("unreadable object: {e}"),
});
continue;
}
};
checked += 1;
match ty {
ObjectType::Commit => match Commit::parse(odb.algo(), &payload) {
Ok(c) => {
queue.push_back((c.tree, Some(id)));
for p in c.parents {
queue.push_back((p, Some(id)));
}
}
Err(e) => problems.push(malformed(id, parent, "commit", e)),
},
ObjectType::Tree => match Tree::parse(odb.algo(), &payload) {
Ok(t) => {
for entry in t.entries {
if entry.mode != FileMode::Gitlink {
queue.push_back((entry.id, Some(id)));
}
}
}
Err(e) => problems.push(malformed(id, parent, "tree", e)),
},
ObjectType::Tag => match Tag::parse(odb.algo(), &payload) {
Ok(t) => queue.push_back((t.object, Some(id))),
Err(e) => problems.push(malformed(id, parent, "tag", e)),
},
ObjectType::Blob => {}
}
}
let mut dangling = Vec::new();
for id in repo.loose_object_ids()? {
if !seen.contains(&id) {
dangling.push(id);
}
}
dangling.sort();
Ok(Report {
checked,
problems,
dangling,
})
}
fn malformed(id: ObjectId, referenced_by: Option<ObjectId>, kind: &str, e: Error) -> Problem {
Problem {
id,
referenced_by,
message: alloc::format!("malformed {kind}: {e}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::object::{ObjectType, Signature};
fn sig() -> Signature {
Signature {
name: b"T".to_vec(),
email: b"t@e".to_vec(),
time: 0,
tz: b"+0000".to_vec(),
}
}
#[test]
fn clean_repo_passes_and_flags_dangling() {
let dir = std::env::temp_dir().join(alloc::format!("puregit-fsck-{}", core::line!()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let repo = Repository::init(&dir).unwrap();
std::fs::write(dir.join("a.txt"), b"hello\n").unwrap();
repo.add_path("a.txt").unwrap();
repo.commit(b"c\n", sig(), sig()).unwrap();
let r = fsck(&repo).unwrap();
assert!(r.is_ok(), "unexpected problems: {:?}", r.problems);
assert!(r.checked >= 3); assert!(r.dangling.is_empty());
let stray = repo
.write_object(ObjectType::Blob, b"nobody points here\n")
.unwrap();
let r = fsck(&repo).unwrap();
assert!(r.is_ok());
assert!(r.dangling.contains(&stray));
let _ = std::fs::remove_dir_all(&dir);
}
}