1use std::path::{Path, PathBuf};
6
7pub const BIRD_DIR_NAME: &str = ".bird";
9
10pub const BIRD_DB_NAME: &str = "bird.duckdb";
12
13#[derive(Debug, Clone)]
15pub struct ProjectInfo {
16 pub root: PathBuf,
18
19 pub bird_dir: PathBuf,
21
22 pub db_path: PathBuf,
24}
25
26impl ProjectInfo {
27 pub fn blobs_dir(&self) -> PathBuf {
29 self.bird_dir.join("blobs").join("content")
30 }
31
32 pub fn is_initialized(&self) -> bool {
34 self.db_path.exists()
35 }
36}
37
38pub 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 if bird_dir.is_dir() {
66 return Some(ProjectInfo {
67 root: current,
68 bird_dir,
69 db_path,
70 });
71 }
72
73 if !current.pop() {
75 break;
76 }
77 }
78
79 None
80}
81
82pub fn find_current_project() -> Option<ProjectInfo> {
86 std::env::current_dir().ok().and_then(|cwd| find_project(&cwd))
87}
88
89pub fn is_in_project(dir: &Path) -> bool {
91 find_project(dir).is_some()
92}
93
94pub 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 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 assert!(!project.is_initialized());
162
163 std::fs::write(&project.db_path, b"").unwrap();
165 assert!(project.is_initialized());
166 }
167}