tsk/
repo_utils.rs

1use std::error::Error;
2use std::path::{Path, PathBuf};
3
4/// Find the root of a git repository starting from the given path.
5///
6/// This function walks up the directory tree from the starting path until it finds
7/// a directory containing a `.git` subdirectory, which indicates the repository root.
8///
9/// # Arguments
10///
11/// * `start_path` - The path to start searching from
12///
13/// # Returns
14///
15/// * `Ok(PathBuf)` - The canonical path to the repository root
16/// * `Err` - If not in a git repository or if an I/O error occurs
17///
18/// # Example
19///
20/// ```no_run
21/// use std::path::Path;
22/// use tsk::repo_utils::find_repository_root;
23///
24/// let repo_root = find_repository_root(Path::new(".")).unwrap();
25/// println!("Repository root: {}", repo_root.display());
26/// ```
27pub fn find_repository_root(start_path: &Path) -> Result<PathBuf, Box<dyn Error>> {
28    let start = if start_path.is_relative() {
29        std::env::current_dir()?.join(start_path)
30    } else {
31        start_path.to_path_buf()
32    };
33
34    let mut current = start.canonicalize()?;
35
36    loop {
37        if current.join(".git").exists() {
38            return Ok(current);
39        }
40
41        match current.parent() {
42            Some(parent) => current = parent.to_path_buf(),
43            None => {
44                return Err(format!(
45                    "Not in a git repository (or any of the parent directories): {}",
46                    start_path.display()
47                )
48                .into());
49            }
50        }
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use std::fs;
58    use std::path::PathBuf;
59    use tempfile::TempDir;
60
61    #[test]
62    fn test_find_repository_root_in_git_repo() {
63        // Create a temporary directory structure
64        let temp_dir = TempDir::new().unwrap();
65        let repo_root = temp_dir.path();
66
67        // Create .git directory
68        fs::create_dir(repo_root.join(".git")).unwrap();
69
70        // Create subdirectories
71        let sub_dir = repo_root.join("src").join("commands");
72        fs::create_dir_all(&sub_dir).unwrap();
73
74        // Test from various locations
75        assert_eq!(
76            find_repository_root(repo_root).unwrap(),
77            repo_root.canonicalize().unwrap()
78        );
79        assert_eq!(
80            find_repository_root(&sub_dir).unwrap(),
81            repo_root.canonicalize().unwrap()
82        );
83        assert_eq!(
84            find_repository_root(&repo_root.join("src")).unwrap(),
85            repo_root.canonicalize().unwrap()
86        );
87    }
88
89    #[test]
90    fn test_find_repository_root_not_in_repo() {
91        // Create a temporary directory without .git
92        let temp_dir = TempDir::new().unwrap();
93        let result = find_repository_root(temp_dir.path());
94
95        assert!(result.is_err());
96        assert!(
97            result
98                .unwrap_err()
99                .to_string()
100                .contains("Not in a git repository")
101        );
102    }
103
104    #[test]
105    fn test_find_repository_root_with_relative_path() {
106        // Create a temporary git repository for testing
107        let temp_dir = TempDir::new().unwrap();
108        let repo_root = temp_dir.path();
109
110        // Create .git directory
111        fs::create_dir(repo_root.join(".git")).unwrap();
112
113        // Create a test file
114        fs::write(repo_root.join("test.txt"), "test content").unwrap();
115
116        // Change to the temp directory for this test
117        let original_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/tmp"));
118
119        if std::env::set_current_dir(repo_root).is_ok() {
120            // Test with relative path from inside the repo
121            let result = find_repository_root(Path::new("."));
122            assert!(result.is_ok());
123
124            let found_root = result.unwrap();
125            assert!(found_root.join(".git").exists());
126
127            // Restore original directory
128            let _ = std::env::set_current_dir(original_dir);
129        } else {
130            // If we can't change directory, test with absolute path
131            let result = find_repository_root(repo_root);
132            assert!(result.is_ok());
133
134            let found_root = result.unwrap();
135            assert_eq!(found_root, repo_root.canonicalize().unwrap());
136        }
137    }
138}