1use crate::error::{Result, WtgError};
2use crate::git::GitRepo;
3use git2::{FetchOptions, RemoteCallbacks, Repository};
4use std::path::{Path, PathBuf};
5use std::process::Command;
6
7pub struct RepoManager {
9 local_path: PathBuf,
10 is_remote: bool,
11 owner: Option<String>,
12 repo_name: Option<String>,
13}
14
15impl RepoManager {
16 pub fn local() -> Result<Self> {
18 let repo = Repository::discover(".").map_err(|_| WtgError::NotInGitRepo)?;
19 let path = repo.workdir().ok_or(WtgError::NotInGitRepo)?.to_path_buf();
20
21 Ok(Self {
22 local_path: path,
23 is_remote: false,
24 owner: None,
25 repo_name: None,
26 })
27 }
28
29 pub fn remote(owner: String, repo: String) -> Result<Self> {
32 let cache_dir = get_cache_dir()?;
33 let repo_cache_path = cache_dir.join(format!("{owner}/{repo}"));
34
35 if repo_cache_path.exists() && Repository::open(&repo_cache_path).is_ok() {
37 if let Err(e) = update_remote_repo(&repo_cache_path) {
39 eprintln!("Warning: Failed to update cached repo: {e}");
40 }
42 } else {
43 clone_remote_repo(&owner, &repo, &repo_cache_path)?;
45 }
46
47 Ok(Self {
48 local_path: repo_cache_path,
49 is_remote: true,
50 owner: Some(owner),
51 repo_name: Some(repo),
52 })
53 }
54
55 pub fn git_repo(&self) -> Result<GitRepo> {
57 GitRepo::from_path(&self.local_path)
58 }
59
60 #[must_use]
62 pub const fn path(&self) -> &PathBuf {
63 &self.local_path
64 }
65
66 #[must_use]
68 pub const fn is_remote(&self) -> bool {
69 self.is_remote
70 }
71
72 #[must_use]
74 pub fn remote_info(&self) -> Option<(String, String)> {
75 if self.is_remote {
76 Some((self.owner.clone()?, self.repo_name.clone()?))
77 } else {
78 None
79 }
80 }
81}
82
83fn get_cache_dir() -> Result<PathBuf> {
85 let cache_dir = dirs::cache_dir()
86 .ok_or_else(|| {
87 WtgError::Io(std::io::Error::new(
88 std::io::ErrorKind::NotFound,
89 "Could not determine cache directory",
90 ))
91 })?
92 .join("wtg")
93 .join("repos");
94
95 if !cache_dir.exists() {
96 std::fs::create_dir_all(&cache_dir)?;
97 }
98
99 Ok(cache_dir)
100}
101
102fn clone_remote_repo(owner: &str, repo: &str, target_path: &Path) -> Result<()> {
104 if let Some(parent) = target_path.parent() {
106 std::fs::create_dir_all(parent)?;
107 }
108
109 let repo_url = format!("https://github.com/{owner}/{repo}.git");
110
111 eprintln!("🔄 Cloning remote repository {repo_url}...");
112
113 match clone_with_filter(&repo_url, target_path) {
115 Ok(()) => {
116 eprintln!("✅ Repository cloned successfully (using filter)");
117 Ok(())
118 }
119 Err(e) => {
120 eprintln!("⚠️ Filter clone failed ({e}), falling back to bare clone...");
121 clone_bare_with_git2(&repo_url, target_path)
123 }
124 }
125}
126
127fn clone_with_filter(repo_url: &str, target_path: &Path) -> Result<()> {
129 let output = Command::new("git")
130 .args([
131 "clone",
132 "--filter=blob:none", "--bare", repo_url,
135 target_path.to_str().ok_or_else(|| {
136 WtgError::Io(std::io::Error::new(
137 std::io::ErrorKind::InvalidInput,
138 "Invalid path",
139 ))
140 })?,
141 ])
142 .output()?;
143
144 if !output.status.success() {
145 let error = String::from_utf8_lossy(&output.stderr);
146 return Err(WtgError::Io(std::io::Error::other(format!(
147 "Failed to clone with filter: {error}"
148 ))));
149 }
150
151 Ok(())
152}
153
154fn clone_bare_with_git2(repo_url: &str, target_path: &Path) -> Result<()> {
156 let callbacks = RemoteCallbacks::new();
158
159 let mut fetch_options = FetchOptions::new();
160 fetch_options.remote_callbacks(callbacks);
161
162 let mut builder = git2::build::RepoBuilder::new();
164 builder.fetch_options(fetch_options);
165 builder.bare(true); builder.clone(repo_url, target_path)?;
170
171 eprintln!("✅ Repository cloned successfully (using bare clone)");
172
173 Ok(())
174}
175
176fn update_remote_repo(repo_path: &PathBuf) -> Result<()> {
178 eprintln!("🔄 Updating cached repository...");
179
180 match fetch_with_subprocess(repo_path) {
182 Ok(()) => {
183 eprintln!("✅ Repository updated");
184 Ok(())
185 }
186 Err(_) => {
187 fetch_with_git2(repo_path)
189 }
190 }
191}
192
193fn fetch_with_subprocess(repo_path: &Path) -> Result<()> {
195 let args = build_fetch_args(repo_path)?;
196
197 let output = Command::new("git").args(&args).output()?;
198
199 if !output.status.success() {
200 let error = String::from_utf8_lossy(&output.stderr);
201 return Err(WtgError::Io(std::io::Error::other(format!(
202 "Failed to fetch: {error}"
203 ))));
204 }
205
206 Ok(())
207}
208
209fn build_fetch_args(repo_path: &Path) -> Result<Vec<String>> {
214 let repo_path = repo_path.to_str().ok_or_else(|| {
215 WtgError::Io(std::io::Error::new(
216 std::io::ErrorKind::InvalidInput,
217 "Invalid path",
218 ))
219 })?;
220
221 Ok(vec![
222 "-C".to_string(),
223 repo_path.to_string(),
224 "fetch".to_string(),
225 "--all".to_string(),
226 "--tags".to_string(),
227 "--force".to_string(),
228 "--prune".to_string(),
229 ])
230}
231
232fn fetch_with_git2(repo_path: &PathBuf) -> Result<()> {
234 let repo = Repository::open(repo_path)?;
235
236 let mut remote = repo
238 .find_remote("origin")
239 .or_else(|_| repo.find_remote("upstream"))
240 .map_err(WtgError::Git)?;
241
242 let callbacks = RemoteCallbacks::new();
244 let mut fetch_options = FetchOptions::new();
245 fetch_options.remote_callbacks(callbacks);
246
247 remote.fetch(
249 &["refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"],
250 Some(&mut fetch_options),
251 None,
252 )?;
253
254 eprintln!("✅ Repository updated");
255
256 Ok(())
257}