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}