circuitpython_deploy/
ignore.rs

1use 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        // Add default ignores
15        Self::add_default_patterns(&mut builder)?;
16        
17        // Add .cpdignore if it exists
18        let cpdignore_path = project_root.join(".cpdignore");
19        if cpdignore_path.exists() {
20            builder.add(&cpdignore_path);
21        }
22        
23        // Add .gitignore if it exists (as additional patterns)
24        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        // Default patterns to ignore
41        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    /// Check if a file should be included (not ignored)
75    pub fn should_include(&self, path: &Path) -> bool {
76        // Convert absolute path to relative path from project root
77        let relative_path = if path.is_absolute() {
78            match path.strip_prefix(&self.project_root) {
79                Ok(rel) => rel,
80                Err(_) => return true, // If not under project root, include by default
81            }
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    /// Get a closure that can be used for filtering
93    pub fn filter_fn(&self) -> impl Fn(&Path) -> bool + '_ {
94        move |path: &Path| self.should_include(path)
95    }
96    
97    /// List all patterns that would be applied
98    #[allow(dead_code)]
99    pub fn list_patterns(&self) -> Vec<String> {
100        // This is a simplified version - the actual gitignore crate doesn't expose patterns directly
101        // In a real implementation, you might want to store the patterns separately
102        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/// Helper function to create a simple filter for testing
113#[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        // Should ignore .git directory
147        assert!(!filter.should_include(&PathBuf::from(".git")));
148        assert!(!filter.should_include(&PathBuf::from(".git/config")));
149        
150        // Should ignore target directory
151        assert!(!filter.should_include(&PathBuf::from("target")));
152        assert!(!filter.should_include(&PathBuf::from("target/debug")));
153        
154        // Should include regular Python files
155        assert!(filter.should_include(&PathBuf::from("main.py")));
156        assert!(filter.should_include(&PathBuf::from("code.py")));
157        
158        // Should ignore compiled Python files
159        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        // Create a .cpdignore file
169        fs::write(&cpdignore_path, "custom_ignore/\n*.log\ntemp_*").unwrap();
170        
171        let filter = IgnoreFilter::new(temp_dir.path()).unwrap();
172        
173        // Should ignore custom patterns
174        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        // Should still include regular files
179        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}