1use std::path::Path;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7pub enum VcsType {
8 Git,
9}
10
11impl std::fmt::Display for VcsType {
12 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13 match self {
14 VcsType::Git => write!(f, "Git"),
15 }
16 }
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub enum BuildSystem {
22 Cargo,
23 Npm,
24 CMake,
25 Go,
26 Python,
27 Zig,
28 Make,
29 Gradle,
30 Maven,
31 Meson,
32}
33
34impl std::fmt::Display for BuildSystem {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 let s = match self {
37 BuildSystem::Cargo => "Cargo",
38 BuildSystem::Npm => "Npm",
39 BuildSystem::CMake => "CMake",
40 BuildSystem::Go => "Go",
41 BuildSystem::Python => "Python",
42 BuildSystem::Zig => "Zig",
43 BuildSystem::Make => "Make",
44 BuildSystem::Gradle => "Gradle",
45 BuildSystem::Maven => "Maven",
46 BuildSystem::Meson => "Meson",
47 };
48 write!(f, "{s}")
49 }
50}
51
52struct BuildSystemInfo {
53 marker: &'static str,
54 system: BuildSystem,
55 artifact_dirs: &'static [&'static str],
56}
57
58const BUILD_SYSTEMS: &[BuildSystemInfo] = &[
59 BuildSystemInfo {
60 marker: "Cargo.toml",
61 system: BuildSystem::Cargo,
62 artifact_dirs: &["target"],
63 },
64 BuildSystemInfo {
65 marker: "package.json",
66 system: BuildSystem::Npm,
67 artifact_dirs: &["node_modules", "dist", "build"],
68 },
69 BuildSystemInfo {
70 marker: "CMakeLists.txt",
71 system: BuildSystem::CMake,
72 artifact_dirs: &["build"],
73 },
74 BuildSystemInfo {
75 marker: "go.mod",
76 system: BuildSystem::Go,
77 artifact_dirs: &[],
78 },
79 BuildSystemInfo {
80 marker: "pyproject.toml",
81 system: BuildSystem::Python,
82 artifact_dirs: &["__pycache__", ".venv", "dist"],
83 },
84 BuildSystemInfo {
85 marker: "build.zig",
86 system: BuildSystem::Zig,
87 artifact_dirs: &["zig-out", "zig-cache"],
88 },
89 BuildSystemInfo {
90 marker: "Makefile",
91 system: BuildSystem::Make,
92 artifact_dirs: &[],
93 },
94 BuildSystemInfo {
95 marker: "build.gradle",
96 system: BuildSystem::Gradle,
97 artifact_dirs: &["build", ".gradle"],
98 },
99 BuildSystemInfo {
100 marker: "build.gradle.kts",
101 system: BuildSystem::Gradle,
102 artifact_dirs: &["build", ".gradle"],
103 },
104 BuildSystemInfo {
105 marker: "pom.xml",
106 system: BuildSystem::Maven,
107 artifact_dirs: &["target"],
108 },
109 BuildSystemInfo {
110 marker: "meson.build",
111 system: BuildSystem::Meson,
112 artifact_dirs: &["builddir"],
113 },
114];
115
116pub const ARTIFACT_DIR_NAMES: &[&str] = &[
118 "target",
119 "node_modules",
120 "dist",
121 "build",
122 "__pycache__",
123 ".venv",
124 "zig-out",
125 "zig-cache",
126 ".gradle",
127 "builddir",
128 ".git",
129];
130
131pub struct DetectionResult {
133 pub vcs: Vec<VcsType>,
134 pub build_systems: Vec<BuildSystem>,
135 pub artifact_dirs: Vec<String>,
136}
137
138pub fn detect_project(path: &Path) -> DetectionResult {
140 let mut vcs = Vec::new();
141 let mut build_systems = Vec::new();
142 let mut artifact_dirs = Vec::new();
143
144 if path.join(".git").exists() {
146 vcs.push(VcsType::Git);
147 }
148
149 for info in BUILD_SYSTEMS {
151 if path.join(info.marker).exists() {
152 if !build_systems.contains(&info.system) {
154 build_systems.push(info.system.clone());
155 }
156 for dir in info.artifact_dirs {
157 let s = dir.to_string();
158 if !artifact_dirs.contains(&s) {
159 artifact_dirs.push(s);
160 }
161 }
162 }
163 }
164
165 DetectionResult {
166 vcs,
167 build_systems,
168 artifact_dirs,
169 }
170}
171
172pub fn is_project(path: &Path) -> bool {
174 if path.join(".git").exists() {
175 return true;
176 }
177 for info in BUILD_SYSTEMS {
178 if path.join(info.marker).exists() {
179 return true;
180 }
181 }
182 false
183}
184
185pub fn scan_projects(root: &Path, max_depth: usize) -> Vec<std::path::PathBuf> {
188 let mut found = Vec::new();
189
190 let walker = walkdir::WalkDir::new(root)
191 .max_depth(max_depth)
192 .follow_links(false)
193 .into_iter();
194
195 let mut project_roots: Vec<std::path::PathBuf> = Vec::new();
197
198 for entry in walker.filter_entry(|e| {
199 if e.depth() == 0 {
201 return true;
202 }
203 if !e.file_type().is_dir() {
205 return false;
206 }
207 if let Some(name) = e.file_name().to_str() {
209 if ARTIFACT_DIR_NAMES.contains(&name) {
210 return false;
211 }
212 if name.starts_with('.') {
214 return false;
215 }
216 }
217 true
218 }) {
219 let Ok(entry) = entry else { continue };
220 if !entry.file_type().is_dir() {
221 continue;
222 }
223 let path = entry.path();
224
225 if project_roots
227 .iter()
228 .any(|root| path.starts_with(root) && path != root)
229 {
230 continue;
231 }
232
233 if is_project(path) {
234 project_roots.push(path.to_path_buf());
235 found.push(path.to_path_buf());
236 }
237 }
238
239 found
240}