circuitpython_deploy/
ignore.rs1use crate::error::{CpdError, Result};
2use ignore::{gitignore::GitignoreBuilder, Match};
3use std::path::{Path, PathBuf};
4
5pub struct IgnoreFilter {
6 gitignore: ignore::gitignore::Gitignore,
7 project_root: PathBuf,
8}
9
10impl IgnoreFilter {
11 pub fn new(project_root: &Path) -> Result<Self> {
12 let mut builder = GitignoreBuilder::new(project_root);
13
14 Self::add_default_patterns(&mut builder)?;
16
17 let cpdignore_path = project_root.join(".cpdignore");
19 if cpdignore_path.exists() {
20 builder.add(&cpdignore_path);
21 }
22
23 let gitignore_path = project_root.join(".gitignore");
25 if gitignore_path.exists() {
26 builder.add(&gitignore_path);
27 }
28
29 let gitignore = builder.build().map_err(|e| CpdError::InvalidIgnorePattern {
30 pattern: e.to_string(),
31 })?;
32
33 Ok(Self {
34 gitignore,
35 project_root: project_root.to_path_buf(),
36 })
37 }
38
39 fn add_default_patterns(builder: &mut GitignoreBuilder) -> Result<()> {
40 let default_patterns = [
42 ".git",
43 ".gitignore",
44 ".cpdignore",
45 "target",
46 "node_modules",
47 ".env",
48 ".env.local",
49 "*.tmp",
50 "*.temp",
51 ".DS_Store",
52 "Thumbs.db",
53 "*.pyc",
54 "__pycache__",
55 ".pytest_cache",
56 ".coverage",
57 ".vscode",
58 ".idea",
59 "*.swp",
60 "*.swo",
61 "*~",
62 ];
63
64 for pattern in &default_patterns {
65 builder.add_line(None, pattern).map_err(|e| CpdError::InvalidIgnorePattern {
66 pattern: format!("Default pattern '{}': {}", pattern, e),
67 })?;
68 }
69
70 Ok(())
71 }
72
73 pub fn should_include(&self, path: &Path) -> bool {
75 let relative_path = if path.is_absolute() {
77 match path.strip_prefix(&self.project_root) {
78 Ok(rel) => rel,
79 Err(_) => return true, }
81 } else {
82 path
83 };
84
85 match self.gitignore.matched(relative_path, path.is_dir()) {
86 Match::None | Match::Whitelist(_) => true,
87 Match::Ignore(_) => false,
88 }
89 }
90
91 pub fn filter_fn(&self) -> impl Fn(&Path) -> bool + '_ {
93 move |path: &Path| self.should_include(path)
94 }
95
96 #[allow(dead_code)]
98 pub fn list_patterns(&self) -> Vec<String> {
99 vec![
102 ".git/".to_string(),
103 "target/".to_string(),
104 "node_modules/".to_string(),
105 "*.pyc".to_string(),
106 "__pycache__/".to_string(),
107 ]
108 }
109}
110
111#[allow(dead_code)]
113pub fn create_simple_filter(patterns: &[&str]) -> Result<impl Fn(&Path) -> bool> {
114 use std::path::Path;
115 let temp_dir = std::env::temp_dir();
116 let mut builder = GitignoreBuilder::new(&temp_dir);
117
118 for pattern in patterns {
119 builder.add_line(None, pattern).map_err(|e| CpdError::InvalidIgnorePattern {
120 pattern: format!("Pattern '{}': {}", pattern, e),
121 })?;
122 }
123
124 let gitignore = builder.build().map_err(|e| CpdError::InvalidIgnorePattern {
125 pattern: e.to_string(),
126 })?;
127
128 Ok(move |path: &Path| {
129 let relative_path = if path.is_absolute() {
131 path.file_name().map(Path::new).unwrap_or(path)
132 } else {
133 path
134 };
135
136 match gitignore.matched(relative_path, false) {
137 Match::None | Match::Whitelist(_) => true,
138 Match::Ignore(_) => false,
139 }
140 })
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use std::fs;
147 use tempfile::TempDir;
148
149 #[test]
150 fn test_default_ignores() {
151 let temp_dir = TempDir::new().unwrap();
152 let filter = IgnoreFilter::new(temp_dir.path()).unwrap();
153
154 let project_root = temp_dir.path();
156
157
158
159 assert!(!filter.should_include(&project_root.join(".git")));
161 assert!(!filter.should_include(&project_root.join("target")));
166 assert!(filter.should_include(&project_root.join("main.py")));
170 assert!(filter.should_include(&project_root.join("code.py")));
171
172 assert!(!filter.should_include(&project_root.join("test.pyc")));
174 assert!(!filter.should_include(&project_root.join("__pycache__")));
175 }
176
177 #[test]
178 fn test_custom_cpdignore() {
179 let temp_dir = TempDir::new().unwrap();
180 let cpdignore_path = temp_dir.path().join(".cpdignore");
181
182 fs::write(&cpdignore_path, "custom_ignore\n*.log\ntemp_*").unwrap();
184
185 let filter = IgnoreFilter::new(temp_dir.path()).unwrap();
186 let project_root = temp_dir.path();
187
188 assert!(!filter.should_include(&project_root.join("custom_ignore")));
190 assert!(!filter.should_include(&project_root.join("debug.log")));
191 assert!(!filter.should_include(&project_root.join("temp_file.txt")));
192
193 assert!(filter.should_include(&project_root.join("main.py")));
195 }
196
197 #[test]
198 fn test_simple_filter() {
199 let filter = create_simple_filter(&["*.txt", "temp"]).unwrap();
200
201 assert!(!filter(&PathBuf::from("readme.txt")));
202 assert!(!filter(&PathBuf::from("temp")));
203 assert!(filter(&PathBuf::from("main.py")));
204 }
205}