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 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 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_HTTPS: &str = "https://github.com/pitoniak32/git_repo.git";
150
151 #[fixture]
152 fn temp_directory_fs() -> TempDir {
153 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 Git::init(temp_directory_fs.path()).expect("git repo should init in temp dir");
161 temp_directory_fs
162 }
163
164 #[rstest]
195 fn test_https_clone_git_repo(temp_directory_fs: TempDir) {
196 let repo =
198 GitRepo::from_url(REPO_CLONE_HTTPS, temp_directory_fs.path()).expect("should not fail");
199
200 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 let remote_urls = [
209 REPO_CLONE_HTTPS,
210 "https://github.com/pitoniak32/actions.git",
211 ];
212
213 GitRepo::from_url_multi(&remote_urls, temp_directory_fs.path());
215
216 assert!(Path::exists(&temp_directory_fs.path().join("git_repo")));
218 assert!(Path::exists(&temp_directory_fs.path().join("actions")));
219 }
220}