1use anyhow::Result;
2use globset::GlobSet;
3use ignore::WalkBuilder;
4use std::path::Path;
5
6use crate::config::compile_globs;
7
8pub fn discover(
11 root: &Path,
12 include_patterns: &[String],
13 exclude_patterns: &[String],
14) -> Result<Vec<String>> {
15 let include_set = compile_globs(include_patterns)?.unwrap_or_else(GlobSet::empty);
16 let exclude_set = compile_globs(exclude_patterns)?;
17
18 let mut files = Vec::new();
19
20 let walker = WalkBuilder::new(root)
21 .follow_links(true)
22 .sort_by_file_name(|a, b| a.cmp(b))
23 .build();
24
25 for entry in walker {
26 let entry = entry?;
27 if !entry.file_type().is_some_and(|ft| ft.is_file()) {
28 continue;
29 }
30 let path = entry.path();
31
32 let relative = path
33 .strip_prefix(root)
34 .expect("path should be under root")
35 .to_string_lossy()
36 .replace('\\', "/");
37
38 if !include_set.is_match(&relative) {
40 continue;
41 }
42
43 if let Some(ref set) = exclude_set
45 && set.is_match(&relative)
46 {
47 continue;
48 }
49
50 files.push(relative);
51 }
52
53 for pattern in include_patterns {
57 if pattern.contains('*') || pattern.contains('?') || pattern.contains('[') {
58 continue;
59 }
60 if files.iter().any(|f| f == pattern) {
61 continue;
62 }
63 let path = root.join(pattern);
64 if path.is_file() {
65 files.push(pattern.to_string());
66 }
67 }
68
69 files.sort();
70 Ok(files)
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use std::fs;
77 use tempfile::TempDir;
78
79 #[test]
80 fn discovers_files_matching_include() {
81 let dir = TempDir::new().unwrap();
82 fs::write(dir.path().join("index.md"), "# Hello").unwrap();
83 fs::write(dir.path().join("setup.md"), "# Setup").unwrap();
84 fs::write(dir.path().join("notes.txt"), "not markdown").unwrap();
85
86 let files = discover(dir.path(), &["*.md".to_string()], &[]).unwrap();
88 assert_eq!(files, vec!["index.md", "setup.md"]);
89
90 let files = discover(dir.path(), &["*".to_string()], &[]).unwrap();
92 assert_eq!(files, vec!["index.md", "notes.txt", "setup.md"]);
93 }
94
95 #[test]
96 fn discovers_through_nested_drft_toml() {
97 let dir = TempDir::new().unwrap();
99 fs::write(dir.path().join("index.md"), "# Root").unwrap();
100
101 let child = dir.path().join("child");
102 fs::create_dir(&child).unwrap();
103 fs::write(child.join("drft.toml"), "").unwrap();
104 fs::write(child.join("inner.md"), "# Inner").unwrap();
105
106 let files = discover(dir.path(), &["**/*.md".to_string()], &[]).unwrap();
107 assert_eq!(files, vec!["child/inner.md", "index.md"]);
108 }
109
110 #[test]
111 fn respects_exclude_patterns() {
112 let dir = TempDir::new().unwrap();
113 fs::write(dir.path().join("index.md"), "# Hello").unwrap();
114 let drafts = dir.path().join("drafts");
115 fs::create_dir(&drafts).unwrap();
116 fs::write(drafts.join("wip.md"), "# WIP").unwrap();
117
118 let files = discover(dir.path(), &["*.md".to_string()], &["drafts/*".to_string()]).unwrap();
119 assert_eq!(files, vec!["index.md"]);
120 }
121
122 #[test]
123 fn respects_gitignore() {
124 let dir = TempDir::new().unwrap();
125 fs::create_dir(dir.path().join(".git")).unwrap();
127 fs::write(dir.path().join(".gitignore"), "vendor/\n").unwrap();
128 fs::write(dir.path().join("index.md"), "# Hello").unwrap();
129 let vendor = dir.path().join("vendor");
130 fs::create_dir(&vendor).unwrap();
131 fs::write(vendor.join("lib.md"), "# Vendored").unwrap();
132
133 let files = discover(dir.path(), &["*.md".to_string()], &[]).unwrap();
134 assert_eq!(files, vec!["index.md"]);
135 }
136
137 #[test]
138 fn multiple_include_patterns() {
139 let dir = TempDir::new().unwrap();
140 fs::write(dir.path().join("index.md"), "# Hello").unwrap();
141 fs::write(dir.path().join("config.yaml"), "key: val").unwrap();
142 fs::write(dir.path().join("notes.txt"), "text").unwrap();
143
144 let files = discover(dir.path(), &["*.md".to_string(), "*.yaml".to_string()], &[]).unwrap();
145 assert_eq!(files, vec!["config.yaml", "index.md"]);
146 }
147}