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 ".git",
44 ".gitignore",
45 ".cpdignore",
46 "target/",
47 "node_modules/",
48 ".env",
49 ".env.local",
50 "*.tmp",
51 "*.temp",
52 ".DS_Store",
53 "Thumbs.db",
54 "*.pyc",
55 "__pycache__/",
56 ".pytest_cache/",
57 ".coverage",
58 ".vscode/",
59 ".idea/",
60 "*.swp",
61 "*.swo",
62 "*~",
63 ];
64
65 for pattern in &default_patterns {
66 builder.add_line(None, pattern).map_err(|e| CpdError::InvalidIgnorePattern {
67 pattern: format!("Default pattern '{}': {}", pattern, e),
68 })?;
69 }
70
71 Ok(())
72 }
73
74 pub fn should_include(&self, path: &Path) -> bool {
76 let relative_path = if path.is_absolute() {
78 match path.strip_prefix(&self.project_root) {
79 Ok(rel) => rel,
80 Err(_) => return true, }
82 } else {
83 path
84 };
85
86 match self.gitignore.matched(relative_path, path.is_dir()) {
87 Match::None | Match::Whitelist(_) => true,
88 Match::Ignore(_) => false,
89 }
90 }
91
92 pub fn filter_fn(&self) -> impl Fn(&Path) -> bool + '_ {
94 move |path: &Path| self.should_include(path)
95 }
96
97 #[allow(dead_code)]
99 pub fn list_patterns(&self) -> Vec<String> {
100 vec![
103 ".git/".to_string(),
104 "target/".to_string(),
105 "node_modules/".to_string(),
106 "*.pyc".to_string(),
107 "__pycache__/".to_string(),
108 ]
109 }
110}
111
112#[allow(dead_code)]
114pub fn create_simple_filter(patterns: &[&str]) -> Result<impl Fn(&Path) -> bool> {
115 let mut builder = GitignoreBuilder::new(".");
116
117 for pattern in patterns {
118 builder.add_line(None, pattern).map_err(|e| CpdError::InvalidIgnorePattern {
119 pattern: format!("Pattern '{}': {}", pattern, e),
120 })?;
121 }
122
123 let gitignore = builder.build().map_err(|e| CpdError::InvalidIgnorePattern {
124 pattern: e.to_string(),
125 })?;
126
127 Ok(move |path: &Path| {
128 match gitignore.matched(path, path.is_dir()) {
129 Match::None | Match::Whitelist(_) => true,
130 Match::Ignore(_) => false,
131 }
132 })
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use std::fs;
139 use tempfile::TempDir;
140
141 #[test]
142 fn test_default_ignores() {
143 let temp_dir = TempDir::new().unwrap();
144 let filter = IgnoreFilter::new(temp_dir.path()).unwrap();
145
146 assert!(!filter.should_include(&PathBuf::from(".git")));
148 assert!(!filter.should_include(&PathBuf::from(".git/config")));
149
150 assert!(!filter.should_include(&PathBuf::from("target")));
152 assert!(!filter.should_include(&PathBuf::from("target/debug")));
153
154 assert!(filter.should_include(&PathBuf::from("main.py")));
156 assert!(filter.should_include(&PathBuf::from("code.py")));
157
158 assert!(!filter.should_include(&PathBuf::from("test.pyc")));
160 assert!(!filter.should_include(&PathBuf::from("__pycache__")));
161 }
162
163 #[test]
164 fn test_custom_cpdignore() {
165 let temp_dir = TempDir::new().unwrap();
166 let cpdignore_path = temp_dir.path().join(".cpdignore");
167
168 fs::write(&cpdignore_path, "custom_ignore/\n*.log\ntemp_*").unwrap();
170
171 let filter = IgnoreFilter::new(temp_dir.path()).unwrap();
172
173 assert!(!filter.should_include(&PathBuf::from("custom_ignore")));
175 assert!(!filter.should_include(&PathBuf::from("debug.log")));
176 assert!(!filter.should_include(&PathBuf::from("temp_file.txt")));
177
178 assert!(filter.should_include(&PathBuf::from("main.py")));
180 }
181
182 #[test]
183 fn test_simple_filter() {
184 let filter = create_simple_filter(&["*.txt", "temp/"]).unwrap();
185
186 assert!(!filter(&PathBuf::from("readme.txt")));
187 assert!(!filter(&PathBuf::from("temp")));
188 assert!(filter(&PathBuf::from("main.py")));
189 }
190}