1use crate::{Error, Result};
6use git2::Repository;
7use std::path::Path;
8
9pub struct RemoteManager {
11 repo_path: std::path::PathBuf,
13}
14
15impl RemoteManager {
16 #[must_use]
18 pub fn new(repo_path: impl AsRef<Path>) -> Self {
19 Self {
20 repo_path: repo_path.as_ref().to_path_buf(),
21 }
22 }
23
24 fn open_repo(&self) -> Result<Repository> {
26 Repository::open(&self.repo_path).map_err(|e| Error::OperationFailed {
27 operation: "open_repository".to_string(),
28 cause: e.to_string(),
29 })
30 }
31
32 pub fn list_remotes(&self) -> Result<Vec<String>> {
38 let repo = self.open_repo()?;
39
40 let remotes = repo.remotes().map_err(|e| Error::OperationFailed {
41 operation: "list_remotes".to_string(),
42 cause: e.to_string(),
43 })?;
44
45 Ok(remotes.iter().filter_map(|r| r.map(String::from)).collect())
46 }
47
48 pub fn get_remote_url(&self, remote_name: &str) -> Result<Option<String>> {
54 let repo = self.open_repo()?;
55
56 match repo.find_remote(remote_name) {
57 Ok(remote) => Ok(remote.url().map(String::from)),
58 Err(e) if e.code() == git2::ErrorCode::NotFound => Ok(None),
59 Err(e) => Err(Error::OperationFailed {
60 operation: "get_remote_url".to_string(),
61 cause: e.to_string(),
62 }),
63 }
64 }
65
66 pub fn remote_exists(&self, remote_name: &str) -> Result<bool> {
72 self.get_remote_url(remote_name).map(|url| url.is_some())
73 }
74
75 pub fn default_remote(&self) -> Result<Option<String>> {
81 let remotes = self.list_remotes()?;
82
83 if remotes.contains(&"origin".to_string()) {
85 return Ok(Some("origin".to_string()));
86 }
87
88 Ok(remotes.into_iter().next())
90 }
91
92 pub fn current_branch(&self) -> Result<Option<String>> {
98 let repo = self.open_repo()?;
99
100 let head = match repo.head() {
101 Ok(h) => h,
102 Err(e) if e.code() == git2::ErrorCode::UnbornBranch => return Ok(None),
103 Err(e) => {
104 return Err(Error::OperationFailed {
105 operation: "get_head".to_string(),
106 cause: e.to_string(),
107 });
108 },
109 };
110
111 if head.is_branch() {
112 Ok(head.shorthand().map(String::from))
113 } else {
114 Ok(None)
115 }
116 }
117
118 pub fn repo_root(&self) -> Result<std::path::PathBuf> {
124 let repo = self.open_repo()?;
125 repo.workdir()
126 .map(std::path::Path::to_path_buf)
127 .ok_or_else(|| Error::OperationFailed {
128 operation: "get_workdir".to_string(),
129 cause: "Repository has no working directory (bare repo)".to_string(),
130 })
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use git2::Signature;
138 use tempfile::TempDir;
139
140 fn create_test_repo() -> (TempDir, Repository) {
141 let dir = TempDir::new().unwrap();
142 let repo = Repository::init(dir.path()).unwrap();
143
144 {
146 let sig = Signature::now("test", "test@test.com").unwrap();
147 let tree_id = repo.index().unwrap().write_tree().unwrap();
148 let tree = repo.find_tree(tree_id).unwrap();
149 repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])
150 .unwrap();
151 }
152
153 (dir, repo)
154 }
155
156 #[test]
157 fn test_remote_manager_creation() {
158 let _manager = RemoteManager::new("/tmp/test");
159 }
161
162 #[test]
163 fn test_list_remotes_empty() {
164 let (dir, _repo) = create_test_repo();
165 let manager = RemoteManager::new(dir.path());
166
167 let remotes = manager.list_remotes().unwrap();
168 assert!(remotes.is_empty());
169 }
170
171 #[test]
172 fn test_list_remotes_with_origin() {
173 let (dir, repo) = create_test_repo();
174
175 repo.remote("origin", "https://github.com/test/test.git")
177 .unwrap();
178
179 let manager = RemoteManager::new(dir.path());
180 let remotes = manager.list_remotes().unwrap();
181
182 assert_eq!(remotes.len(), 1);
183 assert_eq!(remotes[0], "origin");
184 }
185
186 #[test]
187 fn test_get_remote_url() {
188 let (dir, repo) = create_test_repo();
189 let test_url = "https://github.com/test/test.git";
190
191 repo.remote("origin", test_url).unwrap();
192
193 let manager = RemoteManager::new(dir.path());
194 let url = manager.get_remote_url("origin").unwrap();
195
196 assert_eq!(url, Some(test_url.to_string()));
197 }
198
199 #[test]
200 fn test_get_nonexistent_remote_url() {
201 let (dir, _repo) = create_test_repo();
202 let manager = RemoteManager::new(dir.path());
203
204 let url = manager.get_remote_url("nonexistent").unwrap();
205 assert!(url.is_none());
206 }
207
208 #[test]
209 fn test_remote_exists() {
210 let (dir, repo) = create_test_repo();
211 repo.remote("origin", "https://github.com/test/test.git")
212 .unwrap();
213
214 let manager = RemoteManager::new(dir.path());
215
216 assert!(manager.remote_exists("origin").unwrap());
217 assert!(!manager.remote_exists("nonexistent").unwrap());
218 }
219
220 #[test]
221 fn test_default_remote() {
222 let (dir, repo) = create_test_repo();
223
224 let manager = RemoteManager::new(dir.path());
225
226 assert!(manager.default_remote().unwrap().is_none());
228
229 repo.remote("origin", "https://github.com/test/test.git")
231 .unwrap();
232
233 assert_eq!(
234 manager.default_remote().unwrap(),
235 Some("origin".to_string())
236 );
237 }
238
239 #[test]
240 fn test_default_remote_prefers_origin() {
241 let (dir, repo) = create_test_repo();
242
243 repo.remote("upstream", "https://github.com/upstream/test.git")
245 .unwrap();
246 repo.remote("origin", "https://github.com/test/test.git")
247 .unwrap();
248
249 let manager = RemoteManager::new(dir.path());
250
251 assert_eq!(
253 manager.default_remote().unwrap(),
254 Some("origin".to_string())
255 );
256 }
257
258 #[test]
259 fn test_current_branch() {
260 let (dir, _repo) = create_test_repo();
261 let manager = RemoteManager::new(dir.path());
262
263 let branch = manager.current_branch().unwrap();
265 assert!(branch.is_some());
266 }
267
268 #[test]
269 fn test_repo_root() {
270 let (dir, _repo) = create_test_repo();
271 let manager = RemoteManager::new(dir.path());
272
273 let root = manager.repo_root().unwrap();
274 let expected = dir.path().canonicalize().unwrap();
276 let actual = root.canonicalize().unwrap();
277 assert_eq!(actual, expected);
278 }
279}