flatten_rust/
exclusions.rs

1//! Exclusion patterns management module
2//!
3//! This module provides intelligent exclusion patterns based on
4//! gitignore templates and project auto-detection.
5
6use crate::config::GitignoreManager;
7use anyhow::Result;
8use std::collections::HashSet;
9use std::path::Path;
10use std::sync::Arc;
11use tokio::sync::RwLock;
12
13/// Exclusion patterns manager
14///
15/// Manages intelligent exclusion patterns based on gitignore templates
16/// and project auto-detection capabilities
17pub struct ExclusionManager {
18    gitignore_manager: Arc<RwLock<GitignoreManager>>,
19    custom_patterns: HashSet<String>,
20    enabled_templates: HashSet<String>,
21}
22
23impl ExclusionManager {
24    /// Creates a new ExclusionManager instance
25    ///
26    /// # Errors
27    /// Returns an error if the gitignore manager cannot be initialized
28    /// or if template updates fail
29    ///
30    /// # Examples
31    /// ```no_run
32    /// use flatten_rust::exclusions::ExclusionManager;
33    /// 
34    /// # async fn example() -> anyhow::Result<()> {
35    /// let manager = ExclusionManager::new().await?;
36    /// # Ok(())
37    /// # }
38    /// ```
39    pub async fn new() -> Result<Self> {
40        let mut gitignore_manager = GitignoreManager::new()?;
41        
42        // Update templates if needed
43        gitignore_manager.update_if_needed().await?;
44        
45        Ok(Self {
46            gitignore_manager: Arc::new(RwLock::new(gitignore_manager)),
47            custom_patterns: HashSet::new(),
48            enabled_templates: HashSet::new(),
49        })
50    }
51    
52    /// Enable templates based on project detection
53    pub async fn enable_templates_for_project(&mut self, project_path: &Path) -> Result<()> {
54        let manager = self.gitignore_manager.read().await;
55        let available_templates = manager.get_available_templates();
56        
57        // Reset enabled templates
58        self.enabled_templates.clear();
59        
60        // Check for each template if its patterns exist in the project
61        for template_key in available_templates {
62            if self.detect_template_in_project(project_path, template_key).await? {
63                self.enabled_templates.insert(template_key.to_string());
64            }
65        }
66        
67        Ok(())
68    }
69    
70    /// Detect if a template is relevant for the project
71    async fn detect_template_in_project(&self, project_path: &Path, template_key: &str) -> Result<bool> {
72        let manager = self.gitignore_manager.read().await;
73        
74        // Get patterns for this template
75        let patterns = manager.get_patterns_for_templates(&[template_key.to_string()]);
76        
77        // Check if any pattern files/directories exist in the project
78        for pattern in patterns {
79            if self.pattern_exists_in_project(project_path, &pattern) {
80                return Ok(true);
81            }
82        }
83        
84        Ok(false)
85    }
86    
87    /// Check if a pattern exists in the project
88    fn pattern_exists_in_project(&self, project_path: &Path, pattern: &str) -> bool {
89        // Simple pattern matching - check for common project files
90        match pattern.trim() {
91            // File patterns
92            p if p.ends_with("Cargo.toml") => project_path.join("Cargo.toml").exists(),
93            p if p.ends_with("package.json") => project_path.join("package.json").exists(),
94            p if p.ends_with("requirements.txt") => project_path.join("requirements.txt").exists(),
95            p if p.ends_with("pom.xml") => project_path.join("pom.xml").exists(),
96            p if p.ends_with("build.gradle") => project_path.join("build.gradle").exists(),
97            p if p.ends_with("*.csproj") => {
98                // Check for any .csproj file
99                std::fs::read_dir(project_path)
100                    .map(|entries| {
101                        entries.filter_map(|e| e.ok())
102                            .any(|e| e.file_name().to_string_lossy().ends_with(".csproj"))
103                    })
104                    .unwrap_or(false)
105            }
106            p if p.ends_with("go.mod") => project_path.join("go.mod").exists(),
107            p if p.ends_with("Gemfile") => project_path.join("Gemfile").exists(),
108            p if p.ends_with("composer.json") => project_path.join("composer.json").exists(),
109            p if p.ends_with("pubspec.yaml") => project_path.join("pubspec.yaml").exists(),
110            
111            // Directory patterns
112            p if p.contains("node_modules") => project_path.join("node_modules").exists(),
113            p if p.contains("target") => project_path.join("target").exists(),
114            p if p.contains("__pycache__") => project_path.join("__pycache__").exists(),
115            p if p.contains(".gradle") => project_path.join(".gradle").exists(),
116            p if p.contains(".vs") => project_path.join(".vs").exists(),
117            p if p.contains(".idea") => project_path.join(".idea").exists(),
118            
119            _ => false,
120        }
121    }
122    
123    /// Get all exclusion patterns (folders and files)
124    pub async fn get_exclusion_patterns(&self) -> Vec<String> {
125        let manager = self.gitignore_manager.read().await;
126        let mut patterns = Vec::new();
127        
128        // Add patterns from enabled templates
129        let enabled_templates: Vec<String> = self.enabled_templates.iter().cloned().collect();
130        patterns.extend(manager.get_patterns_for_templates(&enabled_templates));
131        
132        // Add custom patterns
133        patterns.extend(self.custom_patterns.iter().cloned());
134        
135        patterns
136    }
137    
138    /// Get folder exclusion patterns
139    pub async fn get_folder_patterns(&self) -> HashSet<String> {
140        let all_patterns = self.get_exclusion_patterns().await;
141        let mut folder_patterns = HashSet::new();
142        
143        for pattern in all_patterns {
144            // Extract folder names from patterns
145            if let Some(folder_name) = self.extract_folder_name(&pattern) {
146                folder_patterns.insert(folder_name);
147            }
148        }
149        
150        folder_patterns
151    }
152    
153    /// Get file extension exclusion patterns
154    pub async fn get_extension_patterns(&self) -> HashSet<String> {
155        let all_patterns = self.get_exclusion_patterns().await;
156        let mut extension_patterns = HashSet::new();
157        
158        for pattern in all_patterns {
159            // Extract file extensions from patterns
160            if let Some(extension) = self.extract_extension(&pattern) {
161                extension_patterns.insert(extension);
162            }
163        }
164        
165        extension_patterns
166    }
167    
168    /// Extract folder name from pattern
169    fn extract_folder_name(&self, pattern: &str) -> Option<String> {
170        let pattern = pattern.trim();
171        
172        // Skip comments and special patterns
173        if pattern.starts_with('#') || pattern.starts_with('!') {
174            return None;
175        }
176        
177        // Handle directory patterns (ending with /)
178        if pattern.ends_with('/') {
179            return Some(pattern.trim_end_matches('/').to_string());
180        }
181        
182        // Handle patterns that look like directories
183        if !pattern.contains('.') && !pattern.contains('*') {
184            return Some(pattern.to_string());
185        }
186        
187        // Extract directory from path patterns
188        if let Some(last_slash) = pattern.rfind('/') {
189            let dir_part = &pattern[..last_slash];
190            if !dir_part.contains('*') && !dir_part.contains('.') {
191                return Some(dir_part.to_string());
192            }
193        }
194        
195        None
196    }
197    
198    /// Extract file extension from pattern
199    fn extract_extension(&self, pattern: &str) -> Option<String> {
200        let pattern = pattern.trim();
201        
202        // Skip comments and special patterns
203        if pattern.starts_with('#') || pattern.starts_with('!') {
204            return None;
205        }
206        
207        // Handle *.ext patterns
208        if pattern.starts_with("*.") && !pattern.contains('/') {
209            return Some(pattern.trim_start_matches("*.").to_string());
210        }
211        
212        // Handle patterns ending with specific extensions
213        if let Some(dot_pos) = pattern.rfind('.') {
214            let after_dot = &pattern[dot_pos + 1..];
215            if !after_dot.contains('/') && !after_dot.contains('*') {
216                return Some(after_dot.to_string());
217            }
218        }
219        
220        None
221    }
222    
223    
224    
225    /// Get list of enabled templates
226    pub fn get_enabled_templates(&self) -> Vec<&str> {
227        self.enabled_templates.iter().map(|s| s.as_str()).collect()
228    }
229    
230    /// Enable specific template
231    pub fn enable_template(&mut self, template_key: String) {
232        self.enabled_templates.insert(template_key);
233    }
234    
235    /// Disable specific template
236    pub fn disable_template(&mut self, template_key: &str) -> bool {
237        self.enabled_templates.remove(template_key)
238    }
239    
240    /// Force update templates
241    pub async fn force_update_templates(&self) -> Result<()> {
242        let manager = Arc::clone(&self.gitignore_manager);
243        let mut manager = manager.write().await;
244        manager.force_update().await
245    }
246    
247    /// Get available templates
248    pub async fn get_available_templates(&self) -> Vec<String> {
249        let manager = self.gitignore_manager.read().await;
250        manager.get_available_templates().into_iter().map(|s| s.to_string()).collect()
251    }
252    
253    
254    
255    /// Set internet connectivity checking
256    pub async fn set_check_internet(&self, check: bool) -> Result<()> {
257        let manager = Arc::clone(&self.gitignore_manager);
258        let mut manager = manager.write().await;
259        manager.set_check_internet(check)
260    }
261}