Skip to main content

ironflow_ops_git/
repo.rs

1//! [`GitRepo`] -- central handle wrapping a repository path.
2
3use std::path::{Path, PathBuf};
4
5use git2::Repository;
6use ironflow_core::error::OperationError;
7
8use crate::helpers::git_error;
9
10/// A handle to a local Git repository, identified by its working directory path.
11///
12/// `GitRepo` does not hold a [`git2::Repository`] directly because `Repository`
13/// is `!Sync` while [`Operation`](ironflow_core::operation::Operation) requires
14/// `Send + Sync`. Instead, each operation re-opens the repository inside
15/// [`spawn_blocking`](tokio::task::spawn_blocking).
16///
17/// # Examples
18///
19/// ```no_run
20/// use ironflow_ops_git::GitRepo;
21///
22/// # fn example() -> Result<(), ironflow_core::error::OperationError> {
23/// let repo = GitRepo::open("/path/to/repo")?;
24/// assert!(repo.path().exists());
25/// # Ok(())
26/// # }
27/// ```
28#[derive(Debug, Clone)]
29pub struct GitRepo {
30    path: PathBuf,
31}
32
33impl GitRepo {
34    /// Open an existing repository at the given path.
35    ///
36    /// The path should point to the working directory (not `.git/`).
37    ///
38    /// # Errors
39    ///
40    /// Returns [`OperationError::External`] if the path is not a valid
41    /// Git repository.
42    ///
43    /// # Examples
44    ///
45    /// ```no_run
46    /// use ironflow_ops_git::GitRepo;
47    ///
48    /// # fn example() -> Result<(), ironflow_core::error::OperationError> {
49    /// let repo = GitRepo::open("/path/to/repo")?;
50    /// # Ok(())
51    /// # }
52    /// ```
53    pub fn open(path: impl AsRef<Path>) -> Result<Self, OperationError> {
54        let path = path.as_ref().to_path_buf();
55        let repo = Repository::open(&path).map_err(git_error)?;
56        let workdir = repo
57            .workdir()
58            .map(Path::to_path_buf)
59            .unwrap_or_else(|| path.clone());
60        Ok(Self { path: workdir })
61    }
62
63    /// Create a `GitRepo` from a path without validation.
64    ///
65    /// Use this when you know the path is valid (e.g. after [`RepoInit`](crate::repository::RepoInit)
66    /// or [`RepoClone`](crate::repository::RepoClone)).
67    pub fn from_path(path: impl Into<PathBuf>) -> Self {
68        Self { path: path.into() }
69    }
70
71    /// The working directory path of this repository.
72    pub fn path(&self) -> &Path {
73        &self.path
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn open_valid_repo() {
83        let tmp = tempfile::tempdir().unwrap();
84        Repository::init(tmp.path()).unwrap();
85        let repo = GitRepo::open(tmp.path()).unwrap();
86        assert!(repo.path().exists());
87        assert!(repo.path().join(".git").exists());
88    }
89
90    #[test]
91    fn open_invalid_path() {
92        let err = GitRepo::open("/nonexistent/path/to/repo").unwrap_err();
93        let msg = err.to_string();
94        assert!(msg.contains("git error"), "got: {msg}");
95    }
96}