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)).map_err(|_| {
59 GitError::CommandFailed(format!("Not a git repository: {}", path_ref.display()))
60 })?;
61
62 Ok(Self {
63 repo_path: path_ref.to_path_buf(),
64 })
65 }
66
67 pub fn init<P: AsRef<Path>>(path: P, bare: bool) -> Result<Self> {
78 Self::ensure_git()?;
79
80 let mut args = vec!["init"];
81 if bare {
82 args.push("--bare");
83 }
84 args.push(path.as_ref().to_str().unwrap_or(""));
85
86 let _stdout = git(&args, None)?;
87
88 Ok(Self {
89 repo_path: path.as_ref().to_path_buf(),
90 })
91 }
92
93 pub fn repo_path(&self) -> &Path {
94 &self.repo_path
95 }
96
97 pub fn config(&self) -> crate::commands::RepoConfig<'_> {
118 crate::commands::RepoConfig::new(self)
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use std::env;
126 use std::fs;
127
128 #[test]
129 fn test_git_init_creates_repository() {
130 let test_path = env::temp_dir().join("test_repo");
131
132 if test_path.exists() {
134 fs::remove_dir_all(&test_path).unwrap();
135 }
136
137 let repo = Repository::init(&test_path, false).unwrap();
139
140 assert!(test_path.join(".git").exists());
142 assert_eq!(repo.repo_path(), test_path.as_path());
143
144 fs::remove_dir_all(&test_path).unwrap();
146 }
147
148 #[test]
149 fn test_git_init_bare_repository() {
150 let test_path = env::temp_dir().join("test_bare_repo");
151
152 if test_path.exists() {
154 fs::remove_dir_all(&test_path).unwrap();
155 }
156
157 let repo = Repository::init(&test_path, true).unwrap();
159
160 assert!(test_path.join("HEAD").exists());
162 assert!(test_path.join("objects").exists());
163 assert!(!test_path.join(".git").exists());
164 assert_eq!(repo.repo_path(), test_path.as_path());
165
166 fs::remove_dir_all(&test_path).unwrap();
168 }
169
170 #[test]
171 fn test_open_existing_repository() {
172 let test_path = env::temp_dir().join("test_open_repo");
173
174 if test_path.exists() {
176 fs::remove_dir_all(&test_path).unwrap();
177 }
178
179 let _created_repo = Repository::init(&test_path, false).unwrap();
181
182 let opened_repo = Repository::open(&test_path).unwrap();
184 assert_eq!(opened_repo.repo_path(), test_path.as_path());
185
186 fs::remove_dir_all(&test_path).unwrap();
188 }
189
190 #[test]
191 fn test_open_nonexistent_path() {
192 let test_path = env::temp_dir().join("nonexistent_repo_path");
193
194 if test_path.exists() {
196 fs::remove_dir_all(&test_path).unwrap();
197 }
198
199 let result = Repository::open(&test_path);
201 assert!(result.is_err());
202
203 if let Err(GitError::CommandFailed(msg)) = result {
204 assert!(msg.contains("Path does not exist"));
205 } else {
206 panic!("Expected CommandFailed error");
207 }
208 }
209
210 #[test]
211 fn test_open_non_git_directory() {
212 let test_path = env::temp_dir().join("not_a_git_repo");
213
214 if test_path.exists() {
216 fs::remove_dir_all(&test_path).unwrap();
217 }
218 fs::create_dir(&test_path).unwrap();
219
220 let result = Repository::open(&test_path);
222 assert!(result.is_err());
223
224 if let Err(GitError::CommandFailed(msg)) = result {
225 assert!(msg.contains("Not a git repository"));
226 } else {
227 panic!("Expected CommandFailed error");
228 }
229
230 fs::remove_dir_all(&test_path).unwrap();
232 }
233
234 #[test]
235 fn test_repo_path_method() {
236 let test_path = env::temp_dir().join("test_repo_path");
237
238 if test_path.exists() {
240 fs::remove_dir_all(&test_path).unwrap();
241 }
242
243 let repo = Repository::init(&test_path, false).unwrap();
245
246 assert_eq!(repo.repo_path(), test_path.as_path());
248
249 fs::remove_dir_all(&test_path).unwrap();
251 }
252
253 #[test]
254 fn test_repo_path_method_after_open() {
255 let test_path = env::temp_dir().join("test_repo_path_open");
256
257 if test_path.exists() {
259 fs::remove_dir_all(&test_path).unwrap();
260 }
261
262 let _created_repo = Repository::init(&test_path, false).unwrap();
264 let opened_repo = Repository::open(&test_path).unwrap();
265
266 assert_eq!(opened_repo.repo_path(), test_path.as_path());
268
269 fs::remove_dir_all(&test_path).unwrap();
271 }
272
273 #[test]
274 fn test_ensure_git_caching() {
275 let result1 = Repository::ensure_git();
277 let result2 = Repository::ensure_git();
278 let result3 = Repository::ensure_git();
279
280 assert!(result1.is_ok());
281 assert!(result2.is_ok());
282 assert!(result3.is_ok());
283 }
284
285 #[test]
286 fn test_init_with_empty_string_path() {
287 let result = Repository::init("", false);
288 let _ = result;
291 }
292
293 #[test]
294 fn test_open_with_empty_string_path() {
295 let result = Repository::open("");
296 assert!(result.is_err());
297
298 match result.unwrap_err() {
299 GitError::CommandFailed(msg) => {
300 assert!(
301 msg.contains("Path does not exist") || msg.contains("Not a git repository")
302 );
303 }
304 _ => panic!("Expected CommandFailed error"),
305 }
306 }
307
308 #[test]
309 fn test_init_with_relative_path() {
310 let test_path = env::temp_dir().join("relative_test_repo");
311
312 if test_path.exists() {
314 fs::remove_dir_all(&test_path).unwrap();
315 }
316
317 let result = Repository::init(&test_path, false);
318
319 if let Ok(repo) = result {
320 assert_eq!(repo.repo_path(), test_path.as_path());
321
322 fs::remove_dir_all(&test_path).unwrap();
324 }
325 }
326
327 #[test]
328 fn test_open_with_relative_path() {
329 let test_path = env::temp_dir().join("relative_open_repo");
330
331 if test_path.exists() {
333 fs::remove_dir_all(&test_path).unwrap();
334 }
335
336 let _created = Repository::init(&test_path, false).unwrap();
338
339 let result = Repository::open(&test_path);
341 assert!(result.is_ok());
342
343 let repo = result.unwrap();
344 assert_eq!(repo.repo_path(), test_path.as_path());
345
346 fs::remove_dir_all(&test_path).unwrap();
348 }
349
350 #[test]
351 fn test_init_with_unicode_path() {
352 let test_path = env::temp_dir().join("测试_repo_🚀");
353
354 if test_path.exists() {
356 fs::remove_dir_all(&test_path).unwrap();
357 }
358
359 let result = Repository::init(&test_path, false);
360
361 if let Ok(repo) = result {
362 assert_eq!(repo.repo_path(), test_path.as_path());
363
364 fs::remove_dir_all(&test_path).unwrap();
366 }
367 }
368
369 #[test]
370 fn test_path_with_spaces() {
371 let test_path = env::temp_dir().join("test repo with spaces");
372
373 if test_path.exists() {
375 fs::remove_dir_all(&test_path).unwrap();
376 }
377
378 let result = Repository::init(&test_path, false);
379
380 if let Ok(repo) = result {
381 assert_eq!(repo.repo_path(), test_path.as_path());
382
383 fs::remove_dir_all(&test_path).unwrap();
385 }
386 }
387
388 #[test]
389 fn test_very_long_path() {
390 let long_component = "a".repeat(100);
391 let test_path = env::temp_dir().join(&long_component);
392
393 if test_path.exists() {
395 fs::remove_dir_all(&test_path).unwrap();
396 }
397
398 let result = Repository::init(&test_path, false);
399
400 if let Ok(repo) = result {
401 assert_eq!(repo.repo_path(), test_path.as_path());
402
403 fs::remove_dir_all(&test_path).unwrap();
405 }
406 }
407}