1use std::path::{Path, PathBuf};
2
3use walkdir::WalkDir;
4
5use crate::config::{ProjectConfig, WorkspaceConfig};
6use crate::error::ConfigError;
7
8pub fn find_workspace_root(start_dir: &Path) -> Result<PathBuf, ConfigError> {
11 let mut current = start_dir.to_path_buf();
12 loop {
13 let candidate = current.join("guild.toml");
14 if candidate.exists() {
15 let content =
16 std::fs::read_to_string(&candidate).map_err(|e| ConfigError::ReadFile {
17 path: candidate.clone(),
18 source: e,
19 })?;
20 if content.contains("[workspace]") {
21 return Ok(current);
22 }
23 }
24 if !current.pop() {
25 return Err(ConfigError::WorkspaceNotFound {
26 path: start_dir.to_path_buf(),
27 });
28 }
29 }
30}
31
32pub fn discover_projects(workspace: &WorkspaceConfig) -> Result<Vec<ProjectConfig>, ConfigError> {
37 let root = workspace.root();
38 let mut projects = Vec::new();
39
40 for pattern in workspace.project_patterns() {
41 let full_pattern = root.join(pattern).to_string_lossy().to_string();
42 let matches = glob::glob(&full_pattern).map_err(|e| ConfigError::ReadFile {
43 path: PathBuf::from(&full_pattern),
44 source: std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string()),
45 })?;
46
47 for entry in matches {
48 let path = entry.map_err(|e| ConfigError::ReadFile {
49 path: PathBuf::from(&full_pattern),
50 source: std::io::Error::other(e.to_string()),
51 })?;
52 if path.is_dir() {
53 let toml_path = path.join("guild.toml");
54 if toml_path.exists() {
55 projects.push(ProjectConfig::from_file(&toml_path)?);
56 }
57 }
58 }
59 }
60
61 if projects.is_empty() && workspace.project_patterns().is_empty() {
63 for entry in WalkDir::new(root)
64 .min_depth(1)
65 .max_depth(3)
66 .into_iter()
67 .filter_map(|e| e.ok())
68 {
69 if entry.file_name() == "guild.toml" && entry.path() != root.join("guild.toml") {
70 let content =
71 std::fs::read_to_string(entry.path()).map_err(|e| ConfigError::ReadFile {
72 path: entry.path().to_path_buf(),
73 source: e,
74 })?;
75 if content.contains("[project]") {
76 projects.push(ProjectConfig::from_file(entry.path())?);
77 }
78 }
79 }
80 }
81
82 Ok(projects)
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use std::fs;
89
90 #[test]
91 fn test_find_workspace_root() {
92 let dir = tempfile::tempdir().unwrap();
93 let root = dir.path();
94
95 fs::write(
97 root.join("guild.toml"),
98 "[workspace]\nname = \"test\"\nprojects = [\"apps/*\"]\n",
99 )
100 .unwrap();
101
102 let nested = root.join("apps").join("my-app");
104 fs::create_dir_all(&nested).unwrap();
105
106 let found = find_workspace_root(&nested).unwrap();
107 assert_eq!(found, root);
108 }
109
110 #[test]
111 fn test_find_workspace_root_not_found() {
112 let dir = tempfile::tempdir().unwrap();
113 assert!(find_workspace_root(dir.path()).is_err());
114 }
115
116 #[test]
117 fn test_discover_projects() {
118 let dir = tempfile::tempdir().unwrap();
119 let root = dir.path();
120
121 fs::write(
123 root.join("guild.toml"),
124 "[workspace]\nname = \"test\"\nprojects = [\"apps/*\"]\n",
125 )
126 .unwrap();
127
128 let app_dir = root.join("apps").join("my-app");
130 fs::create_dir_all(&app_dir).unwrap();
131 fs::write(
132 app_dir.join("guild.toml"),
133 "[project]\nname = \"my-app\"\n\n[targets.build]\ncommand = \"echo build\"\n",
134 )
135 .unwrap();
136
137 let workspace = WorkspaceConfig::from_file(&root.join("guild.toml")).unwrap();
138 let projects = discover_projects(&workspace).unwrap();
139
140 assert_eq!(projects.len(), 1);
141 assert_eq!(projects[0].name().as_str(), "my-app");
142 }
143}