Skip to main content

magic_bird/
project.rs

1//! Project detection for BIRD.
2//!
3//! Finds project-level `.bird/` directories by walking up from the current directory.
4
5use std::path::{Path, PathBuf};
6
7/// The name of the BIRD project directory.
8pub const BIRD_DIR_NAME: &str = ".bird";
9
10/// The name of the project database file.
11pub const BIRD_DB_NAME: &str = "bird.duckdb";
12
13/// Result of project detection.
14#[derive(Debug, Clone)]
15pub struct ProjectInfo {
16    /// Root directory of the project (contains `.bird/`).
17    pub root: PathBuf,
18
19    /// Path to the `.bird/` directory.
20    pub bird_dir: PathBuf,
21
22    /// Path to the project database.
23    pub db_path: PathBuf,
24}
25
26impl ProjectInfo {
27    /// Get the blobs directory for this project.
28    pub fn blobs_dir(&self) -> PathBuf {
29        self.bird_dir.join("blobs").join("content")
30    }
31
32    /// Check if the project database exists.
33    pub fn is_initialized(&self) -> bool {
34        self.db_path.exists()
35    }
36}
37
38/// Find the project root by walking up from the given directory.
39///
40/// Looks for a `.bird/` directory containing a `bird.duckdb` file.
41/// Returns `None` if no project is found before reaching the filesystem root.
42///
43/// # Arguments
44///
45/// * `start_dir` - Directory to start searching from (typically current working directory)
46///
47/// # Example
48///
49/// ```no_run
50/// use bird::project::find_project;
51/// use std::env;
52///
53/// if let Some(project) = find_project(&env::current_dir().unwrap()) {
54///     println!("Found project at: {}", project.root.display());
55/// }
56/// ```
57pub fn find_project(start_dir: &Path) -> Option<ProjectInfo> {
58    let mut current = start_dir.to_path_buf();
59
60    loop {
61        let bird_dir = current.join(BIRD_DIR_NAME);
62        let db_path = bird_dir.join(BIRD_DB_NAME);
63
64        // Check if .bird/ exists (even if not fully initialized)
65        if bird_dir.is_dir() {
66            return Some(ProjectInfo {
67                root: current,
68                bird_dir,
69                db_path,
70            });
71        }
72
73        // Move to parent directory
74        if !current.pop() {
75            break;
76        }
77    }
78
79    None
80}
81
82/// Find project from current working directory.
83///
84/// Convenience function that starts from `std::env::current_dir()`.
85pub fn find_current_project() -> Option<ProjectInfo> {
86    std::env::current_dir().ok().and_then(|cwd| find_project(&cwd))
87}
88
89/// Check if a directory is inside a BIRD project.
90pub fn is_in_project(dir: &Path) -> bool {
91    find_project(dir).is_some()
92}
93
94/// Get the relative path from project root to the given path.
95///
96/// Returns `None` if the path is not under the project root.
97pub fn project_relative_path(project: &ProjectInfo, path: &Path) -> Option<PathBuf> {
98    path.strip_prefix(&project.root).ok().map(|p| p.to_path_buf())
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use tempfile::TempDir;
105
106    #[test]
107    fn test_find_project_exists() {
108        let tmp = TempDir::new().unwrap();
109        let project_root = tmp.path().join("my-project");
110        let bird_dir = project_root.join(".bird");
111        std::fs::create_dir_all(&bird_dir).unwrap();
112
113        // Create a subdirectory to search from
114        let subdir = project_root.join("src").join("lib");
115        std::fs::create_dir_all(&subdir).unwrap();
116
117        let result = find_project(&subdir);
118        assert!(result.is_some());
119
120        let project = result.unwrap();
121        assert_eq!(project.root, project_root);
122        assert_eq!(project.bird_dir, bird_dir);
123    }
124
125    #[test]
126    fn test_find_project_not_found() {
127        let tmp = TempDir::new().unwrap();
128        let result = find_project(tmp.path());
129        assert!(result.is_none());
130    }
131
132    #[test]
133    fn test_project_relative_path() {
134        let project = ProjectInfo {
135            root: PathBuf::from("/home/user/project"),
136            bird_dir: PathBuf::from("/home/user/project/.bird"),
137            db_path: PathBuf::from("/home/user/project/.bird/bird.duckdb"),
138        };
139
140        let abs_path = Path::new("/home/user/project/src/main.rs");
141        let rel = project_relative_path(&project, abs_path);
142        assert_eq!(rel, Some(PathBuf::from("src/main.rs")));
143
144        let outside = Path::new("/other/path");
145        assert!(project_relative_path(&project, outside).is_none());
146    }
147
148    #[test]
149    fn test_is_initialized() {
150        let tmp = TempDir::new().unwrap();
151        let bird_dir = tmp.path().join(".bird");
152        std::fs::create_dir_all(&bird_dir).unwrap();
153
154        let project = ProjectInfo {
155            root: tmp.path().to_path_buf(),
156            bird_dir: bird_dir.clone(),
157            db_path: bird_dir.join("bird.duckdb"),
158        };
159
160        // Not initialized yet
161        assert!(!project.is_initialized());
162
163        // Create the database file
164        std::fs::write(&project.db_path, b"").unwrap();
165        assert!(project.is_initialized());
166    }
167}