1use crate::utils::error::CodeDigestError;
4use std::path::PathBuf;
5use std::process::Command;
6use tempfile::TempDir;
7
8#[cfg(unix)]
9use std::fs;
10
11pub fn gh_available() -> bool {
13 Command::new("gh")
14 .arg("--version")
15 .output()
16 .map(|output| output.status.success())
17 .unwrap_or(false)
18}
19
20pub fn git_available() -> bool {
22 Command::new("git")
23 .arg("--version")
24 .output()
25 .map(|output| output.status.success())
26 .unwrap_or(false)
27}
28
29pub fn parse_github_url(url: &str) -> Result<(String, String), CodeDigestError> {
31 let url = url.trim_end_matches('/');
32
33 let parts: Vec<&str> = if url.starts_with("https://github.com/") {
35 url.strip_prefix("https://github.com/")
36 .ok_or_else(|| CodeDigestError::InvalidConfiguration("Invalid GitHub URL".to_string()))?
37 .split('/')
38 .collect()
39 } else if url.starts_with("http://github.com/") {
40 url.strip_prefix("http://github.com/")
41 .ok_or_else(|| CodeDigestError::InvalidConfiguration("Invalid GitHub URL".to_string()))?
42 .split('/')
43 .collect()
44 } else {
45 return Err(CodeDigestError::InvalidConfiguration(
46 "URL must start with https://github.com/ or http://github.com/".to_string(),
47 ));
48 };
49
50 if parts.len() < 2 {
51 return Err(CodeDigestError::InvalidConfiguration(
52 "GitHub URL must contain owner and repository name".to_string(),
53 ));
54 }
55
56 Ok((parts[0].to_string(), parts[1].to_string()))
57}
58
59pub fn fetch_repository(repo_url: &str, verbose: bool) -> Result<TempDir, CodeDigestError> {
61 let (owner, repo) = parse_github_url(repo_url)?;
62 let temp_dir = TempDir::new().map_err(|e| {
63 CodeDigestError::RemoteFetchError(format!("Failed to create temp directory: {e}"))
64 })?;
65
66 #[cfg(unix)]
68 {
69 use std::os::unix::fs::PermissionsExt;
70 let metadata = fs::metadata(temp_dir.path()).map_err(|e| {
71 CodeDigestError::RemoteFetchError(format!("Failed to get temp directory metadata: {e}"))
72 })?;
73 let mut perms = metadata.permissions();
74 perms.set_mode(0o700);
75 fs::set_permissions(temp_dir.path(), perms).map_err(|e| {
76 CodeDigestError::RemoteFetchError(format!(
77 "Failed to set temp directory permissions: {e}"
78 ))
79 })?;
80 }
81
82 if verbose {
83 eprintln!("📥 Fetching repository: {owner}/{repo}");
84 }
85
86 let success = if gh_available() {
88 if verbose {
89 eprintln!("🔧 Using gh CLI for optimal performance");
90 }
91 clone_with_gh(&owner, &repo, temp_dir.path(), verbose)?
92 } else if git_available() {
93 if verbose {
94 eprintln!("🔧 Using git clone (gh CLI not available)");
95 }
96 clone_with_git(repo_url, temp_dir.path(), verbose)?
97 } else {
98 return Err(CodeDigestError::RemoteFetchError(
99 "Neither gh CLI nor git is available. Please install one of them.".to_string(),
100 ));
101 };
102
103 if !success {
104 return Err(CodeDigestError::RemoteFetchError("Failed to clone repository".to_string()));
105 }
106
107 if verbose {
108 eprintln!("✅ Repository fetched successfully");
109 }
110
111 Ok(temp_dir)
112}
113
114fn clone_with_gh(
116 owner: &str,
117 repo: &str,
118 target_dir: &std::path::Path,
119 verbose: bool,
120) -> Result<bool, CodeDigestError> {
121 let repo_spec = format!("{owner}/{repo}");
122 let mut cmd = Command::new("gh");
123 cmd.arg("repo")
124 .arg("clone")
125 .arg(&repo_spec)
126 .arg(target_dir.join(repo))
127 .arg("--")
128 .arg("--depth")
129 .arg("1");
130
131 if verbose {
132 eprintln!("🔄 Running: gh repo clone {repo_spec} --depth 1");
133 }
134
135 let output = cmd
136 .output()
137 .map_err(|e| CodeDigestError::RemoteFetchError(format!("Failed to run gh: {e}")))?;
138
139 Ok(output.status.success())
140}
141
142fn clone_with_git(
144 repo_url: &str,
145 target_dir: &std::path::Path,
146 verbose: bool,
147) -> Result<bool, CodeDigestError> {
148 let repo_name = repo_url.split('/').next_back().ok_or_else(|| {
149 CodeDigestError::InvalidConfiguration("Invalid repository URL".to_string())
150 })?;
151
152 let mut cmd = Command::new("git");
153 cmd.arg("clone").arg("--depth").arg("1").arg(repo_url).arg(target_dir.join(repo_name));
154
155 if verbose {
156 eprintln!("🔄 Running: git clone --depth 1 {repo_url}");
157 }
158
159 let output = cmd
160 .output()
161 .map_err(|e| CodeDigestError::RemoteFetchError(format!("Failed to run git: {e}")))?;
162
163 Ok(output.status.success())
164}
165
166pub fn get_repo_path(temp_dir: &TempDir, repo_url: &str) -> Result<PathBuf, CodeDigestError> {
168 let (_, repo) = parse_github_url(repo_url)?;
169 let repo_path = temp_dir.path().join(&repo);
170
171 if !repo_path.exists() {
172 return Err(CodeDigestError::RemoteFetchError(format!(
173 "Repository directory not found after cloning: {}",
174 repo_path.display()
175 )));
176 }
177
178 Ok(repo_path)
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_parse_github_url_https() {
187 let (owner, repo) = parse_github_url("https://github.com/rust-lang/rust").unwrap();
188 assert_eq!(owner, "rust-lang");
189 assert_eq!(repo, "rust");
190 }
191
192 #[test]
193 fn test_parse_github_url_http() {
194 let (owner, repo) = parse_github_url("http://github.com/rust-lang/rust").unwrap();
195 assert_eq!(owner, "rust-lang");
196 assert_eq!(repo, "rust");
197 }
198
199 #[test]
200 fn test_parse_github_url_trailing_slash() {
201 let (owner, repo) = parse_github_url("https://github.com/rust-lang/rust/").unwrap();
202 assert_eq!(owner, "rust-lang");
203 assert_eq!(repo, "rust");
204 }
205
206 #[test]
207 fn test_parse_github_url_invalid() {
208 assert!(parse_github_url("https://gitlab.com/rust-lang/rust").is_err());
209 assert!(parse_github_url("not-a-url").is_err());
210 assert!(parse_github_url("https://github.com/").is_err());
211 assert!(parse_github_url("https://github.com/rust-lang").is_err());
212 }
213
214 #[test]
215 fn test_gh_available() {
216 let _ = gh_available();
219 }
220
221 #[test]
222 fn test_git_available() {
223 let _ = git_available();
226 }
227
228 #[test]
229 fn test_get_repo_path() {
230 use std::fs;
231
232 let temp_dir = TempDir::new().unwrap();
233 let repo_url = "https://github.com/owner/repo";
234
235 fs::create_dir_all(temp_dir.path().join("repo")).unwrap();
237
238 let path = get_repo_path(&temp_dir, repo_url).unwrap();
239 assert_eq!(path, temp_dir.path().join("repo"));
240 }
241
242 #[test]
243 fn test_get_repo_path_not_found() {
244 let temp_dir = TempDir::new().unwrap();
245 let repo_url = "https://github.com/owner/repo";
246
247 let result = get_repo_path(&temp_dir, repo_url);
249 assert!(result.is_err());
250 }
251}