libvctrl 0.5.1

A robust, content-addressed version control engine for arbitrary data, designed for embedding into applications.
Documentation
use crate::command::Command;
use crate::domain::hash::Hash;
use crate::error::VctrlError;
use crate::storage::traits::{ObjectStore, ObjectStoreExt, RefStore};
use std::collections::HashSet;

const MAX_BLAME_ITERATIONS: usize = 100_000;

#[derive(Debug, Clone)]
pub struct BlameEntry {
    pub commit_hash: Hash,
    pub blob_hash: Hash,
    pub author: crate::domain::user::UserID,
    pub message: String,
}

pub struct Annotate {
    pub start_commit: Hash,
    pub path: String,
}

impl Command for Annotate {
    type Output = Vec<BlameEntry>;

    fn execute(
        &self,
        store: &mut dyn ObjectStore,
        _refs: &mut dyn RefStore,
    ) -> Result<Vec<BlameEntry>, VctrlError> {
        if self.path.is_empty() {
            return Err(VctrlError::Other("empty path".into()));
        }

        let mut result = Vec::new();
        let mut visited = HashSet::new();
        let mut current = Some(self.start_commit);
        let mut iterations = 0;

        let start_commit = store.get_commit(&self.start_commit)?;
        let start_tree = store.get_tree(&start_commit.tree)?;
        let first_blob = start_tree
            .entries()
            .iter()
            .find(|e| e.name == self.path)
            .ok_or_else(|| VctrlError::NotFound(format!("path '{}' not found", self.path)))?;
        let mut prev_blob_hash = first_blob.hash;
        let mut last_commit_hash = self.start_commit;
        let mut last_commit_info = (
            start_commit.author.clone(),
            start_commit.message.clone(),
            first_blob.hash,
        );

        while let Some(commit_hash) = current {
            iterations += 1;
            if iterations > MAX_BLAME_ITERATIONS {
                return Err(VctrlError::Other(
                    "blame: too many commits to process".into(),
                ));
            }

            if !visited.insert(commit_hash) {
                break;
            }
            let commit = store.get_commit(&commit_hash)?;
            let tree = store.get_tree(&commit.tree)?;

            let entry = match tree.entries().iter().find(|e| e.name == self.path) {
                Some(e) => e,
                None => {
                    break;
                }
            };

            if entry.hash != prev_blob_hash {
                result.push(BlameEntry {
                    commit_hash,
                    blob_hash: entry.hash,
                    author: commit.author.clone(),
                    message: commit.message.clone(),
                });
                prev_blob_hash = entry.hash;
            }

            last_commit_hash = commit_hash;
            last_commit_info = (commit.author.clone(), commit.message.clone(), entry.hash);

            current = commit.parents.first().copied();
        }

        if result.is_empty() || result.last().map(|e| e.commit_hash) != Some(last_commit_hash) {
            result.push(BlameEntry {
                commit_hash: last_commit_hash,
                blob_hash: last_commit_info.2,
                author: last_commit_info.0,
                message: last_commit_info.1,
            });
        }

        Ok(result)
    }
}