Skip to main content

auths_infra_git/
event_log.rs

1use auths_core::ports::storage::{EventLogReader, EventLogWriter, StorageError};
2use auths_keri::Prefix;
3use auths_keri::kel_io::KelStorageError;
4
5use crate::error::map_git2_error;
6use crate::helpers;
7use crate::repo::GitRepo;
8
9const EVENT_FILE: &str = "event.json";
10
11/// Git-backed implementation of `EventLogReader` and `EventLogWriter`.
12///
13/// Events are stored as commits on `refs/keri/<prefix>/kel`. Each commit
14/// contains a single blob named `event.json`. The commit chain forms the
15/// ordered event log for that prefix.
16///
17/// Usage:
18/// ```ignore
19/// use auths_infra_git::{GitRepo, GitEventLog};
20/// use auths_core::ports::storage::EventLogReader;
21///
22/// let repo = GitRepo::open("/path/to/repo")?;
23/// let log = GitEventLog::new(&repo);
24/// let events = log.read_event_log("EAbcdef...")?;
25/// ```
26pub struct GitEventLog<'r> {
27    repo: &'r GitRepo,
28}
29
30impl<'r> GitEventLog<'r> {
31    pub fn new(repo: &'r GitRepo) -> Self {
32        Self { repo }
33    }
34
35    fn kel_ref(prefix: &str) -> String {
36        format!("refs/keri/{}/kel", prefix)
37    }
38}
39
40/// Convert a `StorageError` into `KelStorageError` (identical variant set).
41fn to_kel(e: StorageError) -> KelStorageError {
42    match e {
43        StorageError::NotFound { path } => KelStorageError::NotFound { path },
44        StorageError::AlreadyExists { path } => KelStorageError::AlreadyExists { path },
45        StorageError::CasConflict => KelStorageError::CasConflict,
46        StorageError::Io(s) => KelStorageError::Io(s),
47        StorageError::Internal(e) => KelStorageError::Internal(e),
48        // #[non_exhaustive]: forward any future variants as internal errors
49        other => KelStorageError::Internal(Box::new(other)),
50    }
51}
52
53// auths_core::ports::storage::EventLogReader re-exports auths_keri::kel_io::EventLogReader,
54// so these impls satisfy both paths simultaneously.
55impl EventLogReader for GitEventLog<'_> {
56    fn read_event_log(&self, prefix: &Prefix) -> Result<Vec<u8>, KelStorageError> {
57        let refname = Self::kel_ref(prefix.as_str());
58        self.repo
59            .with_repo(|repo| {
60                let events = walk_commits(repo, &refname)?;
61                let joined: Vec<u8> = events.into_iter().flatten().collect();
62                Ok(joined)
63            })
64            .map_err(to_kel)
65    }
66
67    fn read_event_at(&self, prefix: &Prefix, seq: u128) -> Result<Vec<u8>, KelStorageError> {
68        let refname = Self::kel_ref(prefix.as_str());
69        self.repo
70            .with_repo(|repo| {
71                let events = walk_commits(repo, &refname)?;
72                events.into_iter().nth(seq as usize).ok_or_else(|| {
73                    StorageError::not_found(format!("{}/seq/{}", prefix.as_str(), seq))
74                })
75            })
76            .map_err(to_kel)
77    }
78}
79
80impl EventLogWriter for GitEventLog<'_> {
81    fn append_event(&self, prefix: &Prefix, event: &[u8]) -> Result<(), KelStorageError> {
82        let refname = Self::kel_ref(prefix.as_str());
83        self.repo
84            .with_repo(|repo| {
85                helpers::create_ref_commit(
86                    repo,
87                    &refname,
88                    event,
89                    EVENT_FILE,
90                    &format!("append event to {}", prefix.as_str()),
91                )
92                .map_err(map_git2_error)?;
93                Ok(())
94            })
95            .map_err(to_kel)
96    }
97}
98
99fn walk_commits(repo: &git2::Repository, refname: &str) -> Result<Vec<Vec<u8>>, StorageError> {
100    let reference = match repo.find_reference(refname) {
101        Ok(r) => r,
102        Err(e) if e.code() == git2::ErrorCode::NotFound => return Ok(Vec::new()),
103        Err(e) => return Err(map_git2_error(e)),
104    };
105
106    let mut commit = reference.peel_to_commit().map_err(map_git2_error)?;
107    let mut events = Vec::new();
108
109    loop {
110        let tree_oid = commit.tree_id();
111        match helpers::extract_blob_payload(repo, tree_oid, EVENT_FILE) {
112            Ok(data) => events.push(data),
113            Err(e) => {
114                log::warn!("skipping commit {}: {}", commit.id(), e);
115            }
116        }
117
118        if commit.parent_count() > 0 {
119            commit = commit.parent(0).map_err(map_git2_error)?;
120        } else {
121            break;
122        }
123    }
124
125    events.reverse();
126    Ok(events)
127}