git_lib/
repo.rs

1use std::{
2    fs, io,
3    path::{Path, PathBuf},
4    string::FromUtf8Error,
5};
6use thiserror::Error;
7
8use crate::git::{Git, GitCmdError};
9
10#[derive(Error, Debug)]
11pub enum GitRepoError {
12    #[error("there was an error while cloning {remote_url} to {repo_path}: {source}")]
13    CloneError {
14        remote_url: String,
15        repo_path: String,
16        #[source]
17        source: GitCmdError,
18    },
19
20    #[error("failed to parse string into boolean: {0}")]
21    RepoCheck(#[from] GitCmdError),
22
23    #[error("failed to expand provided repo path: {0}")]
24    RepoPathExpansionError(#[from] io::Error),
25
26    #[error("failed to convert git command output from utf8 into string: {0}")]
27    RepoCheckUtf8Error(#[from] FromUtf8Error),
28
29    #[error("failed to clone git repo into {0}. this path is already a git repo.")]
30    AlreadyExistsError(String),
31
32    #[error("failed to clone git repo with url {0}. invalid remote url.")]
33    InvalidGitRemoteUrl(String),
34}
35
36#[derive(Debug)]
37pub struct GitRepo {
38    pub root_path: PathBuf,
39    pub remote_url: Option<String>,
40}
41
42impl GitRepo {
43    pub fn from_url(remote_url: &str, to_path: &Path) -> Result<GitRepo, GitRepoError> {
44        assert!(
45            !to_path.to_string_lossy().to_string().contains('~'),
46            "repo_path must be absoloute or relative, ~ is not supported"
47        );
48        if !to_path.exists() {
49            fs::create_dir_all(to_path)?;
50        }
51        let expanded_path = &to_path
52            .canonicalize()
53            .map_err(GitRepoError::RepoPathExpansionError)?;
54
55        if Git::is_inside_worktree(&expanded_path) {
56            return Err(GitRepoError::AlreadyExistsError(
57                expanded_path.to_string_lossy().to_string(),
58            ));
59        }
60
61        Git::clone(remote_url, to_path).map_err(|e| GitRepoError::CloneError {
62            remote_url: remote_url.to_string(),
63            repo_path: expanded_path.to_string_lossy().to_string(),
64            source: e,
65        })?;
66
67        GitRepo::from_existing(to_path)
68    }
69
70    /// Will remove the contents of the `to_path` before cloning
71    pub fn from_url_force(remote_url: &str, to_path: &PathBuf) -> Result<GitRepo, GitRepoError> {
72        assert!(
73            !to_path.to_string_lossy().to_string().contains('~'),
74            "repo_path must be absoloute or relative, ~ is not supported"
75        );
76
77        if !to_path.exists() {
78            fs::create_dir_all(to_path)?;
79        } else {
80            fs::remove_dir_all(to_path)?;
81            fs::create_dir_all(to_path)?;
82        }
83
84        let expanded_path = &to_path
85            .canonicalize()
86            .map_err(GitRepoError::RepoPathExpansionError)?;
87
88        Git::clone(remote_url, to_path).map_err(|e| GitRepoError::CloneError {
89            remote_url: remote_url.to_string(),
90            repo_path: expanded_path.to_string_lossy().to_string(),
91            source: e,
92        })?;
93
94        GitRepo::from_existing(to_path)
95    }
96
97    pub fn from_url_multi(
98        remote_urls: &[&str],
99        to_root_path: &Path,
100    ) -> Vec<Result<GitRepo, GitRepoError>> {
101        let mut repo_results = vec![];
102        for remote_url in remote_urls {
103            if let Ok(parsed_uri) = Git::parse_url(remote_url) {
104                repo_results.push(GitRepo::from_url(
105                    remote_url,
106                    &to_root_path.join(parsed_uri.name),
107                ));
108            } else {
109                repo_results.push(Err(GitRepoError::InvalidGitRemoteUrl(
110                    remote_url.to_string(),
111                )));
112            }
113        }
114        repo_results
115    }
116
117    /// Sets remote_url to value of `origin`.
118    pub fn from_existing(repo_path: &Path) -> Result<GitRepo, GitRepoError> {
119        assert!(
120            !repo_path.to_string_lossy().to_string().contains('~'),
121            "repo_path must be absoloute or relative, ~ is not supported"
122        );
123        let expanded_path =
124            std::fs::canonicalize(repo_path).map_err(GitRepoError::RepoPathExpansionError)?;
125
126        if Git::is_inside_worktree(&expanded_path) {
127            Ok(GitRepo {
128                root_path: expanded_path.clone(),
129                remote_url: Git::get_remote_url("origin", &expanded_path)?,
130            })
131        } else {
132            todo!()
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139
140    use std::path::Path;
141
142    use super::*;
143
144    use assert_fs::*;
145
146    use rstest::{fixture, rstest};
147
148    // const REPO_CLONE_SSH: &str = "git@github.com:pitoniak32/git_repo.git";
149    const REPO_CLONE_HTTPS: &str = "https://github.com/pitoniak32/git_repo.git";
150
151    #[fixture]
152    fn temp_directory_fs() -> TempDir {
153        // Arrange
154        TempDir::new().expect("should be able to make temp dir")
155    }
156
157    #[fixture]
158    fn temp_repo_fs(temp_directory_fs: TempDir) -> TempDir {
159        // Arrange
160        Git::init(temp_directory_fs.path()).expect("git repo should init in temp dir");
161        temp_directory_fs
162    }
163
164    // #[rstest]
165    // fn should_clone_into_directory(temp_directory_fs: TempDir) -> Result<()> {
166    //     // Arrange / Act
167    //     let repo = GitRepo::from_ssh_uri(REPO_CLONE_SSH, &temp_directory_fs.path())
168    //         .expect("should not fail");
169    //
170    //     // Assert
171    //     assert_eq!(
172    //         repo.remote_url,
173    //         Some(REPO_CLONE_SSH.to_string())
174    //     );
175    //     assert!(Path::exists(&repo.root_path));
176    //
177    //     Ok(())
178    // }
179    //
180    // #[rstest]
181    // fn test_ssh_clone_git_repo(temp_directory_fs: TempDir) {
182    //     // Act
183    //     let repo = GitRepo::from_ssh_uri(REPO_CLONE_SSH, temp_directory_fs.path())
184    //         .expect("should not fail");
185    //
186    //     // Assert
187    //     assert_eq!(
188    //         repo.remote_url,
189    //         Some(REPO_CLONE_SSH.to_string())
190    //     );
191    //     assert!(Path::exists(&repo.root_path));
192    // }
193
194    #[rstest]
195    fn test_https_clone_git_repo(temp_directory_fs: TempDir) {
196        // Arrange / Act
197        let repo =
198            GitRepo::from_url(REPO_CLONE_HTTPS, temp_directory_fs.path()).expect("should not fail");
199
200        // Assert
201        assert_eq!(repo.remote_url, Some(REPO_CLONE_HTTPS.to_string()));
202        assert!(Path::exists(&repo.root_path));
203    }
204
205    #[rstest]
206    fn test_https_clone_multi_git_repo(temp_directory_fs: TempDir) {
207        // Arrange
208        let remote_urls = [
209            REPO_CLONE_HTTPS,
210            "https://github.com/pitoniak32/actions.git",
211        ];
212
213        // Act
214        GitRepo::from_url_multi(&remote_urls, temp_directory_fs.path());
215
216        // Assert
217        assert!(Path::exists(&temp_directory_fs.path().join("git_repo")));
218        assert!(Path::exists(&temp_directory_fs.path().join("actions")));
219    }
220}