1use std::path::Path;
2use std::path::PathBuf;
3use std::sync::OnceLock;
4
5use crate::error::{GitError, Result};
6use crate::utils::{git, git_raw};
7
8static GIT_CHECKED: OnceLock<Result<()>> = OnceLock::new();
9
10#[derive(Debug)]
11pub struct Repository {
12 repo_path: PathBuf,
13}
14
15impl Repository {
16 pub fn ensure_git() -> Result<()> {
26 GIT_CHECKED
27 .get_or_init(|| {
28 git_raw(&["--version"], None)
29 .map_err(|_| GitError::CommandFailed("Git not found in PATH".to_string()))
30 .map(|_| ())
31 })
32 .clone()
33 }
34
35 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
45 Self::ensure_git()?;
46
47 let path_ref = path.as_ref();
48
49 if !path_ref.exists() {
51 return Err(GitError::CommandFailed(format!(
52 "Path does not exist: {}",
53 path_ref.display()
54 )));
55 }
56
57 let _stdout = git(&["status", "--porcelain"], Some(path_ref))
59 .map_err(|_| GitError::CommandFailed(format!(
60 "Not a git repository: {}",
61 path_ref.display()
62 )))?;
63
64 Ok(Self {
65 repo_path: path_ref.to_path_buf(),
66 })
67 }
68
69 pub fn init<P: AsRef<Path>>(path: P, bare: bool) -> Result<Self> {
80 Self::ensure_git()?;
81
82 let mut args = vec!["init"];
83 if bare {
84 args.push("--bare");
85 }
86 args.push(path.as_ref().to_str().unwrap_or(""));
87
88 let _stdout = git(&args, None)?;
89
90 Ok(Self {
91 repo_path: path.as_ref().to_path_buf(),
92 })
93 }
94
95 pub fn repo_path(&self) -> &Path {
96 &self.repo_path
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use std::fs;
104 use std::path::Path;
105
106 #[test]
107 fn test_git_init_creates_repository() {
108 let test_path = "/tmp/test_repo";
109
110 if Path::new(test_path).exists() {
112 fs::remove_dir_all(test_path).unwrap();
113 }
114
115 let repo = Repository::init(test_path, false).unwrap();
117
118 assert!(Path::new(&format!("{}/.git", test_path)).exists());
120 assert_eq!(repo.repo_path(), Path::new(test_path));
121
122 fs::remove_dir_all(test_path).unwrap();
124 }
125
126 #[test]
127 fn test_git_init_bare_repository() {
128 let test_path = "/tmp/test_bare_repo";
129
130 if Path::new(test_path).exists() {
132 fs::remove_dir_all(test_path).unwrap();
133 }
134
135 let repo = Repository::init(test_path, true).unwrap();
137
138 assert!(Path::new(&format!("{}/HEAD", test_path)).exists());
140 assert!(Path::new(&format!("{}/objects", test_path)).exists());
141 assert!(!Path::new(&format!("{}/.git", test_path)).exists());
142 assert_eq!(repo.repo_path(), Path::new(test_path));
143
144 fs::remove_dir_all(test_path).unwrap();
146 }
147
148 #[test]
149 fn test_open_existing_repository() {
150 let test_path = "/tmp/test_open_repo";
151
152 if Path::new(test_path).exists() {
154 fs::remove_dir_all(test_path).unwrap();
155 }
156
157 let _created_repo = Repository::init(test_path, false).unwrap();
159
160 let opened_repo = Repository::open(test_path).unwrap();
162 assert_eq!(opened_repo.repo_path(), Path::new(test_path));
163
164 fs::remove_dir_all(test_path).unwrap();
166 }
167
168 #[test]
169 fn test_open_nonexistent_path() {
170 let test_path = "/tmp/nonexistent_repo_path";
171
172 if Path::new(test_path).exists() {
174 fs::remove_dir_all(test_path).unwrap();
175 }
176
177 let result = Repository::open(test_path);
179 assert!(result.is_err());
180
181 if let Err(GitError::CommandFailed(msg)) = result {
182 assert!(msg.contains("Path does not exist"));
183 } else {
184 panic!("Expected CommandFailed error");
185 }
186 }
187
188 #[test]
189 fn test_open_non_git_directory() {
190 let test_path = "/tmp/not_a_git_repo";
191
192 if Path::new(test_path).exists() {
194 fs::remove_dir_all(test_path).unwrap();
195 }
196 fs::create_dir(test_path).unwrap();
197
198 let result = Repository::open(test_path);
200 assert!(result.is_err());
201
202 if let Err(GitError::CommandFailed(msg)) = result {
203 assert!(msg.contains("Not a git repository"));
204 } else {
205 panic!("Expected CommandFailed error");
206 }
207
208 fs::remove_dir_all(test_path).unwrap();
210 }
211
212 #[test]
213 fn test_repo_path_method() {
214 let test_path = "/tmp/test_repo_path";
215
216 if Path::new(test_path).exists() {
218 fs::remove_dir_all(test_path).unwrap();
219 }
220
221 let repo = Repository::init(test_path, false).unwrap();
223
224 assert_eq!(repo.repo_path(), Path::new(test_path));
226
227 fs::remove_dir_all(test_path).unwrap();
229 }
230
231 #[test]
232 fn test_repo_path_method_after_open() {
233 let test_path = "/tmp/test_repo_path_open";
234
235 if Path::new(test_path).exists() {
237 fs::remove_dir_all(test_path).unwrap();
238 }
239
240 let _created_repo = Repository::init(test_path, false).unwrap();
242 let opened_repo = Repository::open(test_path).unwrap();
243
244 assert_eq!(opened_repo.repo_path(), Path::new(test_path));
246
247 fs::remove_dir_all(test_path).unwrap();
249 }
250
251 #[test]
252 fn test_ensure_git_caching() {
253 let result1 = Repository::ensure_git();
255 let result2 = Repository::ensure_git();
256 let result3 = Repository::ensure_git();
257
258 assert!(result1.is_ok());
259 assert!(result2.is_ok());
260 assert!(result3.is_ok());
261 }
262
263 #[test]
264 fn test_init_with_empty_string_path() {
265 let result = Repository::init("", false);
266 let _ = result;
269 }
270
271 #[test]
272 fn test_open_with_empty_string_path() {
273 let result = Repository::open("");
274 assert!(result.is_err());
275
276 match result.unwrap_err() {
277 GitError::CommandFailed(msg) => {
278 assert!(msg.contains("Path does not exist") || msg.contains("Not a git repository"));
279 },
280 _ => panic!("Expected CommandFailed error"),
281 }
282 }
283
284 #[test]
285 fn test_init_with_relative_path() {
286 let test_path = "relative_test_repo";
287
288 if Path::new(test_path).exists() {
290 fs::remove_dir_all(test_path).unwrap();
291 }
292
293 let result = Repository::init(test_path, false);
294
295 if result.is_ok() {
296 let repo = result.unwrap();
297 assert_eq!(repo.repo_path(), Path::new(test_path));
298
299 fs::remove_dir_all(test_path).unwrap();
301 }
302 }
303
304 #[test]
305 fn test_open_with_relative_path() {
306 let test_path = "relative_open_repo";
307
308 if Path::new(test_path).exists() {
310 fs::remove_dir_all(test_path).unwrap();
311 }
312
313 let _created = Repository::init(test_path, false).unwrap();
315
316 let result = Repository::open(test_path);
318 assert!(result.is_ok());
319
320 let repo = result.unwrap();
321 assert_eq!(repo.repo_path(), Path::new(test_path));
322
323 fs::remove_dir_all(test_path).unwrap();
325 }
326
327 #[test]
328 fn test_init_with_unicode_path() {
329 let test_path = "/tmp/测试_repo_🚀";
330
331 if Path::new(test_path).exists() {
333 fs::remove_dir_all(test_path).unwrap();
334 }
335
336 let result = Repository::init(test_path, false);
337
338 if result.is_ok() {
339 let repo = result.unwrap();
340 assert_eq!(repo.repo_path(), Path::new(test_path));
341
342 fs::remove_dir_all(test_path).unwrap();
344 }
345 }
346
347 #[test]
348 fn test_path_with_spaces() {
349 let test_path = "/tmp/test repo with spaces";
350
351 if Path::new(test_path).exists() {
353 fs::remove_dir_all(test_path).unwrap();
354 }
355
356 let result = Repository::init(test_path, false);
357
358 if result.is_ok() {
359 let repo = result.unwrap();
360 assert_eq!(repo.repo_path(), Path::new(test_path));
361
362 fs::remove_dir_all(test_path).unwrap();
364 }
365 }
366
367 #[test]
368 fn test_very_long_path() {
369 let long_component = "a".repeat(100);
370 let test_path = format!("/tmp/{}", long_component);
371
372 if Path::new(&test_path).exists() {
374 fs::remove_dir_all(&test_path).unwrap();
375 }
376
377 let result = Repository::init(&test_path, false);
378
379 if result.is_ok() {
380 let repo = result.unwrap();
381 assert_eq!(repo.repo_path(), Path::new(&test_path));
382
383 fs::remove_dir_all(&test_path).unwrap();
385 }
386 }
387}