auths-infra-git 0.0.1-rc.10

Git client adapter for Auths - enables communication with Auths services over Git
Documentation
use auths_core::ports::storage::{EventLogReader, EventLogWriter, StorageError};
use auths_verifier::keri::Prefix;

use crate::error::map_git2_error;
use crate::helpers;
use crate::repo::GitRepo;

const EVENT_FILE: &str = "event.json";

/// Git-backed implementation of `EventLogReader` and `EventLogWriter`.
///
/// Events are stored as commits on `refs/keri/<prefix>/kel`. Each commit
/// contains a single blob named `event.json`. The commit chain forms the
/// ordered event log for that prefix.
///
/// Usage:
/// ```ignore
/// use auths_infra_git::{GitRepo, GitEventLog};
/// use auths_core::ports::storage::EventLogReader;
///
/// let repo = GitRepo::open("/path/to/repo")?;
/// let log = GitEventLog::new(&repo);
/// let events = log.read_event_log("EAbcdef...")?;
/// ```
pub struct GitEventLog<'r> {
    repo: &'r GitRepo,
}

impl<'r> GitEventLog<'r> {
    pub fn new(repo: &'r GitRepo) -> Self {
        Self { repo }
    }

    fn kel_ref(prefix: &str) -> String {
        format!("refs/keri/{}/kel", prefix)
    }
}

impl EventLogReader for GitEventLog<'_> {
    fn read_event_log(&self, prefix: &Prefix) -> Result<Vec<u8>, StorageError> {
        let refname = Self::kel_ref(prefix.as_str());
        self.repo.with_repo(|repo| {
            let events = walk_commits(repo, &refname)?;
            let joined: Vec<u8> = events.into_iter().flatten().collect();
            Ok(joined)
        })
    }

    fn read_event_at(&self, prefix: &Prefix, seq: u64) -> Result<Vec<u8>, StorageError> {
        let refname = Self::kel_ref(prefix.as_str());
        self.repo.with_repo(|repo| {
            let events = walk_commits(repo, &refname)?;
            events
                .into_iter()
                .nth(seq as usize)
                .ok_or_else(|| StorageError::not_found(format!("{}/seq/{}", prefix.as_str(), seq)))
        })
    }
}

impl EventLogWriter for GitEventLog<'_> {
    fn append_event(&self, prefix: &Prefix, event: &[u8]) -> Result<(), StorageError> {
        let refname = Self::kel_ref(prefix.as_str());
        self.repo.with_repo(|repo| {
            helpers::create_ref_commit(
                repo,
                &refname,
                event,
                EVENT_FILE,
                &format!("append event to {}", prefix.as_str()),
            )
            .map_err(map_git2_error)?;
            Ok(())
        })
    }
}

fn walk_commits(repo: &git2::Repository, refname: &str) -> Result<Vec<Vec<u8>>, StorageError> {
    let reference = match repo.find_reference(refname) {
        Ok(r) => r,
        Err(e) if e.code() == git2::ErrorCode::NotFound => return Ok(Vec::new()),
        Err(e) => return Err(map_git2_error(e)),
    };

    let mut commit = reference.peel_to_commit().map_err(map_git2_error)?;
    let mut events = Vec::new();

    loop {
        let tree_oid = commit.tree_id();
        match helpers::extract_blob_payload(repo, tree_oid, EVENT_FILE) {
            Ok(data) => events.push(data),
            Err(e) => {
                log::warn!("skipping commit {}: {}", commit.id(), e);
            }
        }

        if commit.parent_count() > 0 {
            commit = commit.parent(0).map_err(map_git2_error)?;
        } else {
            break;
        }
    }

    events.reverse();
    Ok(events)
}