rustic_git/
repository.rs

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    /// Ensure that Git is available in the system PATH.
17    ///
18    /// This function checks if the `git` command is available in the system PATH.
19    /// The result is cached, so subsequent calls are very fast.
20    /// If Git is not found, it returns a `GitError::CommandFailed` with an appropriate error message.
21    ///
22    /// # Returns
23    ///
24    /// A `Result` containing either `Ok(())` if Git is available or a `GitError`.
25    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    /// Open an existing Git repository at the specified path.
36    ///
37    /// # Arguments
38    ///
39    /// * `path` - The path to an existing Git repository.
40    ///
41    /// # Returns
42    ///
43    /// A `Result` containing either the opened `Repository` instance or a `GitError`.
44    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
45        Self::ensure_git()?;
46
47        let path_ref = path.as_ref();
48
49        // Check if the path exists
50        if !path_ref.exists() {
51            return Err(GitError::CommandFailed(format!(
52                "Path does not exist: {}",
53                path_ref.display()
54            )));
55        }
56
57        // Check if it's a valid git repository by running git status
58        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    /// Initialize a new Git repository at the specified path.
68    ///
69    /// # Arguments
70    ///
71    /// * `path` - The path where the repository should be initialized.
72    /// * `bare` - Whether the repository should be bare or not.
73    ///
74    /// # Returns
75    ///
76    /// A `Result` containing either the initialized `Repository` instance or a `GitError`.
77    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    /// Get a configuration manager for this repository
98    ///
99    /// Returns a `RepoConfig` instance that can be used to get and set
100    /// git configuration values for this repository.
101    ///
102    /// # Example
103    ///
104    /// ```rust
105    /// use rustic_git::Repository;
106    /// use std::env;
107    ///
108    /// let test_path = env::temp_dir().join("test");
109    /// let repo = Repository::init(&test_path, false)?;
110    /// repo.config().set_user("John Doe", "john@example.com")?;
111    ///
112    /// let (name, email) = repo.config().get_user()?;
113    /// assert_eq!(name, "John Doe");
114    /// assert_eq!(email, "john@example.com");
115    /// # Ok::<(), rustic_git::GitError>(())
116    /// ```
117    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        // Clean up if exists
133        if test_path.exists() {
134            fs::remove_dir_all(&test_path).unwrap();
135        }
136
137        // Initialize repository
138        let repo = Repository::init(&test_path, false).unwrap();
139
140        // Check that .git directory was created
141        assert!(test_path.join(".git").exists());
142        assert_eq!(repo.repo_path(), test_path.as_path());
143
144        // Clean up
145        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        // Clean up if exists
153        if test_path.exists() {
154            fs::remove_dir_all(&test_path).unwrap();
155        }
156
157        // Initialize bare repository
158        let repo = Repository::init(&test_path, true).unwrap();
159
160        // Check that bare repo files were created (no .git subdirectory)
161        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        // Clean up
167        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        // Clean up if exists
175        if test_path.exists() {
176            fs::remove_dir_all(&test_path).unwrap();
177        }
178
179        // First create a repository
180        let _created_repo = Repository::init(&test_path, false).unwrap();
181
182        // Now open the existing repository
183        let opened_repo = Repository::open(&test_path).unwrap();
184        assert_eq!(opened_repo.repo_path(), test_path.as_path());
185
186        // Clean up
187        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        // Ensure path doesn't exist
195        if test_path.exists() {
196            fs::remove_dir_all(&test_path).unwrap();
197        }
198
199        // Try to open non-existent repository
200        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        // Clean up if exists and create a regular directory
215        if test_path.exists() {
216            fs::remove_dir_all(&test_path).unwrap();
217        }
218        fs::create_dir(&test_path).unwrap();
219
220        // Try to open directory that's not a git repository
221        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        // Clean up
231        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        // Clean up if exists
239        if test_path.exists() {
240            fs::remove_dir_all(&test_path).unwrap();
241        }
242
243        // Initialize repository
244        let repo = Repository::init(&test_path, false).unwrap();
245
246        // Test repo_path method
247        assert_eq!(repo.repo_path(), test_path.as_path());
248
249        // Clean up
250        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        // Clean up if exists
258        if test_path.exists() {
259            fs::remove_dir_all(&test_path).unwrap();
260        }
261
262        // Initialize and then open repository
263        let _created_repo = Repository::init(&test_path, false).unwrap();
264        let opened_repo = Repository::open(&test_path).unwrap();
265
266        // Test repo_path method on opened repository
267        assert_eq!(opened_repo.repo_path(), test_path.as_path());
268
269        // Clean up
270        fs::remove_dir_all(&test_path).unwrap();
271    }
272
273    #[test]
274    fn test_ensure_git_caching() {
275        // Call ensure_git multiple times to test caching
276        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        // This might succeed or fail depending on git's behavior with empty paths
289        // The important thing is it doesn't panic
290        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        // Clean up if exists
313        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            // Clean up
323            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        // Clean up if exists
332        if test_path.exists() {
333            fs::remove_dir_all(&test_path).unwrap();
334        }
335
336        // Create the repo first
337        let _created = Repository::init(&test_path, false).unwrap();
338
339        // Now open with relative path
340        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        // Clean up
347        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        // Clean up if exists
355        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            // Clean up
365            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        // Clean up if exists
374        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            // Clean up
384            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        // Clean up if exists
394        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            // Clean up
404            fs::remove_dir_all(&test_path).unwrap();
405        }
406    }
407}