pub mod rule;
pub mod rules;
pub mod config;
pub use rules::heading_utils::{HeadingStyle, Heading};
pub use rules::*;
pub fn collect_gitignore_patterns(start_dir: &str) -> Vec<String> {
use std::path::Path;
use std::fs;
let mut patterns = Vec::new();
let path = Path::new(start_dir);
let mut current_dir = if path.is_file() {
path.parent().unwrap_or(Path::new(".")).to_path_buf()
} else {
path.to_path_buf()
};
let mut visited_dirs = std::collections::HashSet::new();
while visited_dirs.insert(current_dir.clone()) {
let gitignore_path = current_dir.join(".gitignore");
if gitignore_path.exists() && gitignore_path.is_file() {
if let Ok(content) = fs::read_to_string(&gitignore_path) {
for line in content.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() && !trimmed.starts_with('#') {
let pattern = normalize_gitignore_pattern(trimmed);
if !pattern.is_empty() {
patterns.push(pattern);
}
}
}
}
}
let git_dir = current_dir.join(".git");
if git_dir.exists() && git_dir.is_dir() {
let exclude_path = git_dir.join("info/exclude");
if exclude_path.exists() && exclude_path.is_file() {
if let Ok(content) = fs::read_to_string(&exclude_path) {
for line in content.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() && !trimmed.starts_with('#') {
let pattern = normalize_gitignore_pattern(trimmed);
if !pattern.is_empty() {
patterns.push(pattern);
}
}
}
}
}
}
match current_dir.parent() {
Some(parent) => current_dir = parent.to_path_buf(),
None => break,
}
}
let common_patterns = vec![
"node_modules",
".git",
".github",
".vscode",
".idea",
"dist",
"build",
"target",
];
for pattern in common_patterns {
if !patterns.iter().any(|p| p == pattern) {
patterns.push(pattern.to_string());
}
}
patterns
}
fn normalize_gitignore_pattern(pattern: &str) -> String {
let mut normalized = pattern.trim().to_string();
if normalized.starts_with('/') {
normalized = normalized[1..].to_string();
}
if normalized.ends_with('/') && normalized.len() > 1 {
normalized = normalized[..normalized.len() - 1].to_string();
}
if normalized.starts_with('!') {
return String::new();
}
if normalized.contains("**") {
return normalized;
}
if !normalized.contains('/') && !normalized.contains('*') {
normalized
} else {
normalized
}
}
pub fn should_exclude(file_path: &str, exclude_patterns: &[String]) -> bool {
use glob::Pattern;
let normalized_path = if file_path.starts_with("./") {
&file_path[2..]
} else {
file_path
};
for pattern in exclude_patterns {
if pattern.ends_with('/') {
let dir_prefix = &pattern[..pattern.len() - 1];
if normalized_path == dir_prefix || normalized_path.starts_with(&format!("{}/", dir_prefix)) {
return true;
}
continue;
}
match Pattern::new(pattern) {
Ok(glob_pattern) => {
if glob_pattern.matches(normalized_path) {
return true;
}
if !pattern.contains('*') && normalized_path.starts_with(&format!("{}/", pattern)) {
return true;
}
if pattern.contains('*') && !pattern.contains("**") {
let pattern_parts: Vec<&str> = pattern.split('/').collect();
let path_parts: Vec<&str> = normalized_path.split('/').collect();
if pattern_parts.len() == path_parts.len() {
let mut all_match = true;
for (i, pattern_part) in pattern_parts.iter().enumerate() {
if let Ok(part_pattern) = Pattern::new(pattern_part) {
if !part_pattern.matches(path_parts[i]) {
all_match = false;
break;
}
} else if pattern_part != &path_parts[i] {
all_match = false;
break;
}
}
if all_match {
return true;
}
}
}
},
Err(_) => {
if normalized_path.contains(pattern) {
return true;
}
}
}
}
false
}