Skip to main content

auths_infra_git/
repo.rs

1use auths_core::ports::storage::StorageError;
2use git2::Repository;
3use std::path::{Path, PathBuf};
4use std::sync::Mutex;
5
6/// Newtype wrapper around `git2::Repository`.
7///
8/// Wraps the repository in a `Mutex` to satisfy `Send + Sync` bounds
9/// required by the storage port traits, since `git2::Repository` is
10/// not `Sync` by default.
11///
12/// Usage:
13/// ```ignore
14/// use auths_infra_git::GitRepo;
15///
16/// let repo = GitRepo::open("/path/to/repo")?;
17/// ```
18pub struct GitRepo {
19    inner: Mutex<Repository>,
20    path: PathBuf,
21}
22
23impl GitRepo {
24    /// Opens an existing Git repository at the given path.
25    ///
26    /// Args:
27    /// * `path`: Filesystem path to the repository root.
28    ///
29    /// Usage:
30    /// ```ignore
31    /// let repo = GitRepo::open("/home/user/.auths")?;
32    /// ```
33    pub fn open(path: impl AsRef<Path>) -> Result<Self, StorageError> {
34        let path = path.as_ref().to_path_buf();
35        let inner = Repository::open(&path).map_err(|e| StorageError::Io(e.to_string()))?;
36        Ok(Self {
37            inner: Mutex::new(inner),
38            path,
39        })
40    }
41
42    /// Initializes a new Git repository at the given path.
43    ///
44    /// Args:
45    /// * `path`: Filesystem path where the repository will be created.
46    ///
47    /// Usage:
48    /// ```ignore
49    /// let repo = GitRepo::init("/tmp/new-repo")?;
50    /// ```
51    pub fn init(path: impl AsRef<Path>) -> Result<Self, StorageError> {
52        let path = path.as_ref().to_path_buf();
53        let inner = Repository::init(&path).map_err(|e| StorageError::Io(e.to_string()))?;
54        disable_gc(&inner)?;
55        Ok(Self {
56            inner: Mutex::new(inner),
57            path,
58        })
59    }
60
61    pub(crate) fn with_repo<T>(
62        &self,
63        f: impl FnOnce(&Repository) -> Result<T, StorageError>,
64    ) -> Result<T, StorageError> {
65        let repo = self
66            .inner
67            .lock()
68            .map_err(|e| StorageError::Io(format!("mutex poisoned: {}", e)))?;
69        f(&repo)
70    }
71
72    pub fn path(&self) -> &Path {
73        &self.path
74    }
75}
76
77/// Disable automatic garbage collection and object pruning on a repository.
78///
79/// Identity KELs are stored as Git objects; an automatic `git gc` that prunes
80/// an unreferenced object is silent identity loss. Every Auths repo is created
81/// with `gc.auto = 0` and `gc.pruneExpire = never` so objects are retained
82/// regardless of reachability.
83///
84/// Args:
85/// * `repo`: The freshly-initialized repository to configure.
86fn disable_gc(repo: &Repository) -> Result<(), StorageError> {
87    let mut cfg = repo.config().map_err(|e| StorageError::Io(e.to_string()))?;
88    cfg.set_i32("gc.auto", 0)
89        .map_err(|e| StorageError::Io(e.to_string()))?;
90    cfg.set_str("gc.pruneExpire", "never")
91        .map_err(|e| StorageError::Io(e.to_string()))?;
92    Ok(())
93}
94
95#[cfg(test)]
96#[allow(clippy::unwrap_used)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn init_disables_gc() {
102        let dir = tempfile::tempdir().unwrap();
103        let repo = GitRepo::init(dir.path()).unwrap();
104        repo.with_repo(|r| {
105            let cfg = r.config().map_err(|e| StorageError::Io(e.to_string()))?;
106            assert_eq!(cfg.get_i32("gc.auto").unwrap(), 0);
107            assert_eq!(cfg.get_string("gc.pruneExpire").unwrap(), "never");
108            Ok(())
109        })
110        .unwrap();
111    }
112}