//! A library for performing object database integrity and connectivity checks #![deny(unsafe_code, missing_docs)] use std::collections::VecDeque; use gix_hash::ObjectId; use gix_hashtable::HashSet; use gix_object::{Exists, FindExt, Kind, tree::EntryKind}; /// Perform a connectivity check. pub struct Connectivity where T: FindExt + Exists, F: FnMut(&ObjectId, Kind), { /// ODB handle to use for the check db: T, /// Closure to invoke when a missing object is encountered missing_cb: F, /// Set of Object IDs already (or about to be) scanned during the check seen: HashSet, /// A buffer to keep a single object at a time. buf: Vec, } impl Connectivity where T: FindExt + Exists, F: FnMut(&ObjectId, Kind), { /// Instantiate a connectivity check. pub fn new(db: T, missing_cb: F) -> Connectivity { Connectivity { db, missing_cb, seen: HashSet::default(), buf: Default::default(), } } /// Run the connectivity check on the provided commit `oid`. /// /// ### Algorithm /// /// Walk the trees and blobs referenced by the commit and verify they exist in the ODB. /// Any objects previously encountered by this instance will be skipped silently. /// Any referenced blobs that are not present in the ODB will result in a call to the `missing_cb`. /// Missing commits or trees will cause an error to be returned. /// - TODO: consider how to handle a missing commit (invoke `missing_cb`, or possibly return a Result?) pub fn check_commit(&mut self, oid: &ObjectId) -> Result<(), gix_object::find::existing_object::Error> { // Attempt to insert the commit ID in the set, and if already present, return immediately if !self.seen.insert(*oid) { return Ok(()); } // Obtain the commit's tree ID let tree_id = { let commit = self.db.find_commit(oid, &mut self.buf)?; commit.tree() }; let mut tree_ids = VecDeque::from_iter(Some(tree_id)); while let Some(tree_id) = tree_ids.pop_front() { if self.seen.insert(tree_id) { self.check_tree(&tree_id, &mut tree_ids); } } Ok(()) } /// Blobs are checked right away, trees are stored in `tree_ids` for the parent to iterate them, and only /// if they have not been `seen` yet. fn check_tree(&mut self, oid: &ObjectId, tree_ids: &mut VecDeque) { let Ok(tree) = self.db.find_tree(oid, &mut self.buf) else { (self.missing_cb)(oid, Kind::Tree); return; }; for entry_ref in tree.entries.iter() { match entry_ref.mode.kind() { EntryKind::Tree => { let tree_id = entry_ref.oid.to_owned(); tree_ids.push_back(tree_id); } EntryKind::Blob | EntryKind::BlobExecutable | EntryKind::Link => { let blob_id = entry_ref.oid.to_owned(); if self.seen.insert(blob_id) { check_blob(&self.db, &blob_id, &mut self.missing_cb); } } EntryKind::Commit => { // Skip submodules as they wouldn't be in this repository! } } } } } fn check_blob(db: impl Exists, oid: &ObjectId, mut missing_cb: F) where F: FnMut(&ObjectId, Kind), { if !db.exists(oid) { missing_cb(oid, Kind::Blob); } }