1use std::path::{Path, PathBuf};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct DiscoveredProject {
11 pub root: PathBuf,
13 pub name: String,
15}
16
17const MAX_DEPTH: usize = 10;
19
20pub fn discover_project(cwd: &Path) -> Option<DiscoveredProject> {
26 let mut current = cwd.to_path_buf();
27
28 for _ in 0..=MAX_DEPTH {
29 if current.join(".git").exists() {
30 let name = current
31 .file_name()
32 .map(|n| n.to_string_lossy().to_string())
33 .unwrap_or_else(|| "unnamed".to_string());
34
35 return Some(DiscoveredProject {
36 root: current,
37 name,
38 });
39 }
40
41 if !current.pop() {
42 break;
43 }
44
45 if current == Path::new("/") || current == Path::new("") {
47 break;
48 }
49 }
50
51 None
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57 use std::fs;
58 use tempfile::TempDir;
59
60 #[test]
61 fn discovers_git_repo_at_cwd() {
62 let tmp = TempDir::new().unwrap();
63 fs::create_dir(tmp.path().join(".git")).unwrap();
64
65 let result = discover_project(tmp.path()).unwrap();
66 assert_eq!(result.root, tmp.path());
67 assert!(!result.name.is_empty());
68 }
69
70 #[test]
71 fn discovers_git_repo_from_subdirectory() {
72 let tmp = TempDir::new().unwrap();
73 fs::create_dir(tmp.path().join(".git")).unwrap();
74 let sub = tmp.path().join("src").join("components");
75 fs::create_dir_all(&sub).unwrap();
76
77 let result = discover_project(&sub).unwrap();
78 assert_eq!(result.root, tmp.path());
79 }
80
81 #[test]
82 fn returns_none_without_git() {
83 let tmp = TempDir::new().unwrap();
84 let sub = tmp.path().join("some").join("deep").join("path");
85 fs::create_dir_all(&sub).unwrap();
86
87 assert!(discover_project(&sub).is_none());
88 }
89
90 #[test]
91 fn discovers_at_max_depth_boundary() {
92 let tmp = TempDir::new().unwrap();
93 let project_dir = tmp.path().join("my-project");
94 fs::create_dir_all(project_dir.join(".git")).unwrap();
95
96 let mut deep = project_dir.clone();
98 for i in 0..MAX_DEPTH {
99 deep = deep.join(format!("level{i}"));
100 }
101 fs::create_dir_all(&deep).unwrap();
102
103 let result = discover_project(&deep);
104 assert!(result.is_some(), "should discover at exactly MAX_DEPTH");
105 assert_eq!(result.unwrap().root, project_dir);
106 }
107
108 #[test]
109 fn uses_directory_name_as_project_name() {
110 let tmp = TempDir::new().unwrap();
111 let project_dir = tmp.path().join("my-awesome-app");
112 fs::create_dir_all(project_dir.join(".git")).unwrap();
113
114 let result = discover_project(&project_dir).unwrap();
115 assert_eq!(result.name, "my-awesome-app");
116 }
117}