1use anyhow::Result;
2use globset::GlobSet;
3use ignore::WalkBuilder;
4use std::path::Path;
5
6use crate::config::compile_globs;
7
8pub fn discover(
12 root: &Path,
13 include_patterns: &[String],
14 exclude_patterns: &[String],
15) -> Result<Vec<String>> {
16 let include_set = compile_globs(include_patterns)?.unwrap_or_else(GlobSet::empty);
17 let exclude_set = compile_globs(exclude_patterns)?;
18
19 let mut files = Vec::new();
20 let root_owned = root.to_path_buf();
21
22 let walker = WalkBuilder::new(root)
23 .follow_links(true)
24 .sort_by_file_name(|a, b| a.cmp(b))
25 .filter_entry(move |entry| {
26 if entry.file_type().is_some_and(|ft| ft.is_dir()) {
27 if entry.path() == root_owned {
28 return true;
29 }
30 if entry.path().join("drft.toml").exists() {
32 return false;
33 }
34 }
35 true
36 })
37 .build();
38
39 for entry in walker {
40 let entry = entry?;
41 if !entry.file_type().is_some_and(|ft| ft.is_file()) {
42 continue;
43 }
44 let path = entry.path();
45
46 let relative = path
47 .strip_prefix(root)
48 .expect("path should be under root")
49 .to_string_lossy()
50 .replace('\\', "/");
51
52 if !include_set.is_match(&relative) {
54 continue;
55 }
56
57 if let Some(ref set) = exclude_set
59 && set.is_match(&relative)
60 {
61 continue;
62 }
63
64 files.push(relative);
65 }
66
67 files.sort();
68 Ok(files)
69}
70
71pub fn find_child_graphs(root: &Path, exclude_patterns: &[String]) -> Result<Vec<String>> {
76 let ignore_set = compile_globs(exclude_patterns)?;
77
78 let mut child_graphs = Vec::new();
79 let root_owned = root.to_path_buf();
80
81 let walker = WalkBuilder::new(root)
84 .follow_links(true)
85 .sort_by_file_name(|a, b| a.cmp(b))
86 .filter_entry(move |entry| {
87 if !entry.file_type().is_some_and(|ft| ft.is_dir()) {
88 return false; }
90 if entry.path() == root_owned {
91 return true;
92 }
93 true
95 })
96 .build();
97
98 let mut found_prefixes: Vec<String> = Vec::new();
99
100 for entry in walker.filter_map(|e| e.ok()) {
101 if !entry.file_type().is_some_and(|ft| ft.is_dir()) {
102 continue;
103 }
104 if entry.path() == root {
105 continue;
106 }
107
108 let relative = entry
109 .path()
110 .strip_prefix(root)
111 .expect("path should be under root")
112 .to_string_lossy()
113 .replace('\\', "/");
114
115 let inside_existing = found_prefixes
117 .iter()
118 .any(|s| relative == s.as_str() || relative.starts_with(&format!("{s}/")));
119 if inside_existing {
120 continue;
121 }
122
123 if entry.path().join("drft.toml").exists() {
124 if let Some(ref set) = ignore_set
126 && set.is_match(&relative)
127 {
128 continue;
129 }
130
131 found_prefixes.push(relative.clone());
132 child_graphs.push(relative);
133 }
134 }
135
136 child_graphs.sort();
137 Ok(child_graphs)
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use std::fs;
144 use tempfile::TempDir;
145
146 #[test]
147 fn discovers_files_matching_include() {
148 let dir = TempDir::new().unwrap();
149 fs::write(dir.path().join("index.md"), "# Hello").unwrap();
150 fs::write(dir.path().join("setup.md"), "# Setup").unwrap();
151 fs::write(dir.path().join("notes.txt"), "not markdown").unwrap();
152
153 let files = discover(dir.path(), &["*.md".to_string()], &[]).unwrap();
155 assert_eq!(files, vec!["index.md", "setup.md"]);
156
157 let files = discover(dir.path(), &["*".to_string()], &[]).unwrap();
159 assert_eq!(files, vec!["index.md", "notes.txt", "setup.md"]);
160 }
161
162 #[test]
163 fn stops_at_graph_boundary() {
164 let dir = TempDir::new().unwrap();
165 fs::write(dir.path().join("index.md"), "# Root").unwrap();
166
167 let child = dir.path().join("child");
168 fs::create_dir(&child).unwrap();
169 fs::write(child.join("drft.toml"), "").unwrap();
170 fs::write(child.join("inner.md"), "# Inner").unwrap();
171
172 let files = discover(dir.path(), &["*.md".to_string()], &[]).unwrap();
173 assert_eq!(files, vec!["index.md"]);
174 }
175
176 #[test]
177 fn respects_exclude_patterns() {
178 let dir = TempDir::new().unwrap();
179 fs::write(dir.path().join("index.md"), "# Hello").unwrap();
180 let drafts = dir.path().join("drafts");
181 fs::create_dir(&drafts).unwrap();
182 fs::write(drafts.join("wip.md"), "# WIP").unwrap();
183
184 let files = discover(dir.path(), &["*.md".to_string()], &["drafts/*".to_string()]).unwrap();
185 assert_eq!(files, vec!["index.md"]);
186 }
187
188 #[test]
189 fn respects_gitignore() {
190 let dir = TempDir::new().unwrap();
191 fs::create_dir(dir.path().join(".git")).unwrap();
193 fs::write(dir.path().join(".gitignore"), "vendor/\n").unwrap();
194 fs::write(dir.path().join("index.md"), "# Hello").unwrap();
195 let vendor = dir.path().join("vendor");
196 fs::create_dir(&vendor).unwrap();
197 fs::write(vendor.join("lib.md"), "# Vendored").unwrap();
198
199 let files = discover(dir.path(), &["*.md".to_string()], &[]).unwrap();
200 assert_eq!(files, vec!["index.md"]);
201 }
202
203 #[test]
204 fn multiple_include_patterns() {
205 let dir = TempDir::new().unwrap();
206 fs::write(dir.path().join("index.md"), "# Hello").unwrap();
207 fs::write(dir.path().join("config.yaml"), "key: val").unwrap();
208 fs::write(dir.path().join("notes.txt"), "text").unwrap();
209
210 let files = discover(dir.path(), &["*.md".to_string(), "*.yaml".to_string()], &[]).unwrap();
211 assert_eq!(files, vec!["config.yaml", "index.md"]);
212 }
213
214 #[test]
215 fn finds_child_graphs() {
216 let dir = TempDir::new().unwrap();
217 fs::write(dir.path().join("index.md"), "# Root").unwrap();
218
219 let alpha = dir.path().join("alpha");
220 fs::create_dir(&alpha).unwrap();
221 fs::write(alpha.join("drft.toml"), "").unwrap();
222
223 let beta = dir.path().join("beta");
224 fs::create_dir(&beta).unwrap();
225 fs::write(beta.join("drft.toml"), "").unwrap();
226
227 let gamma = dir.path().join("gamma");
229 fs::create_dir(&gamma).unwrap();
230 fs::write(gamma.join("readme.md"), "").unwrap();
231
232 let child_graphs = find_child_graphs(dir.path(), &[]).unwrap();
233 assert_eq!(child_graphs, vec!["alpha", "beta"]);
234 }
235
236 #[test]
237 fn child_graphs_stops_at_boundary() {
238 let dir = TempDir::new().unwrap();
239 let child = dir.path().join("child");
240 fs::create_dir(&child).unwrap();
241 fs::write(child.join("drft.toml"), "").unwrap();
242
243 let grandchild = child.join("nested");
245 fs::create_dir(&grandchild).unwrap();
246 fs::write(grandchild.join("drft.toml"), "").unwrap();
247
248 let child_graphs = find_child_graphs(dir.path(), &[]).unwrap();
249 assert_eq!(child_graphs, vec!["child"]);
250 }
251}