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