use colored::Colorize;
use std::path::PathBuf;
pub struct PathCollectionOptions {
pub staged: bool,
pub since: Option<String>,
pub modified: bool,
pub no_default_excludes: bool,
pub no_gitignore: bool,
pub exclude: Vec<String>,
pub paths: Vec<PathBuf>,
pub verbose: bool,
}
pub enum PathCollectionResult {
Success(Vec<PathBuf>, Vec<String>),
Empty(String),
Error(String, i32),
}
pub fn build_exclusion_patterns(options: &PathCollectionOptions) -> Vec<String> {
let mut exclude_patterns: Vec<String> = if options.no_default_excludes {
Vec::new()
} else {
linthis::utils::DEFAULT_EXCLUDES
.iter()
.map(|s| s.to_string())
.collect()
};
if !options.no_gitignore && linthis::utils::is_git_repo() {
let project_root = linthis::utils::get_project_root();
let gitignore_patterns = linthis::utils::get_gitignore_patterns(&project_root);
if options.verbose && !gitignore_patterns.is_empty() {
eprintln!(
"Loaded {} patterns from .gitignore",
gitignore_patterns.len()
);
}
exclude_patterns.extend(gitignore_patterns);
}
exclude_patterns.extend(options.exclude.clone());
let project_root = linthis::utils::get_project_root();
if let Some(project_config) = linthis::config::Config::load_project_config(&project_root) {
if !project_config.excludes.is_empty() {
if options.verbose {
eprintln!(
"Loaded {} exclude patterns from config",
project_config.excludes.len()
);
}
exclude_patterns.extend(project_config.excludes);
}
}
exclude_patterns
}
pub fn filter_files_with_exclusions(
files: Vec<PathBuf>,
exclude_patterns: &[String],
project_root: &PathBuf,
verbose: bool,
) -> Vec<PathBuf> {
use linthis::utils::walker::build_glob_set;
let glob_set = build_glob_set(exclude_patterns);
files
.into_iter()
.filter(|path| {
if let Some(ref gs) = glob_set {
if let Ok(relative) = path.strip_prefix(project_root) {
if gs.is_match(relative) {
if verbose {
eprintln!("Excluding: {}", relative.display());
}
return false;
}
let components: Vec<_> = relative.components().collect();
for i in 0..components.len() {
let subpath: PathBuf = components[i..].iter().collect();
if gs.is_match(&subpath) {
if verbose {
eprintln!(
"Excluding: {} (matches from subpath {})",
relative.display(),
subpath.display()
);
}
return false;
}
}
}
}
true
})
.collect()
}
struct GitCollectMessages<'a> {
empty: &'a str,
empty_after: &'a str,
verbose_prefix: &'a str,
error_label: &'a str,
}
fn collect_git_files<F>(
fetch_fn: F,
exclude_patterns: &[String],
project_root: &PathBuf,
verbose: bool,
msgs: &GitCollectMessages<'_>,
) -> PathCollectionResult
where
F: FnOnce() -> Result<Vec<PathBuf>, Box<dyn std::error::Error>>,
{
match fetch_fn() {
Ok(files) => {
if files.is_empty() {
return PathCollectionResult::Empty(msgs.empty.yellow().to_string());
}
let filtered =
filter_files_with_exclusions(files, exclude_patterns, project_root, verbose);
if filtered.is_empty() {
return PathCollectionResult::Empty(msgs.empty_after.yellow().to_string());
}
if verbose {
eprintln!("{}{}", msgs.verbose_prefix, filtered.len());
}
PathCollectionResult::Success(filtered, exclude_patterns.to_vec())
}
Err(e) => {
PathCollectionResult::Error(format!("{}: {}", msgs.error_label.red(), e), 2)
}
}
}
pub fn collect_paths(options: &PathCollectionOptions) -> PathCollectionResult {
let exclude_patterns = build_exclusion_patterns(options);
let project_root = linthis::utils::get_project_root();
if options.staged {
collect_git_files(
|| linthis::utils::get_staged_files().map_err(|e| e.into()),
&exclude_patterns,
&project_root,
options.verbose,
&GitCollectMessages {
empty: "No staged files to check",
empty_after: "No staged files to check after exclusions",
verbose_prefix: "Checking staged file(s) after exclusions: ",
error_label: "Error getting staged files",
},
)
} else if let Some(ref base_ref) = options.since {
let br = base_ref.clone();
let empty = format!("No files changed since '{}'", base_ref);
let empty_after = format!(
"No files to check after exclusions (since '{}')",
base_ref
);
let verbose_prefix = format!(
"Checking file(s) changed since '{}' after exclusions: ",
base_ref
);
collect_git_files(
|| linthis::utils::get_changed_files(Some(br.as_str())).map_err(|e| e.into()),
&exclude_patterns,
&project_root,
options.verbose,
&GitCollectMessages {
empty: &empty,
empty_after: &empty_after,
verbose_prefix: &verbose_prefix,
error_label: "Error getting changed files",
},
)
} else if options.modified {
collect_git_files(
|| linthis::utils::get_uncommitted_files().map_err(|e| e.into()),
&exclude_patterns,
&project_root,
options.verbose,
&GitCollectMessages {
empty: "No uncommitted files to check",
empty_after: "No uncommitted files to check after exclusions",
verbose_prefix: "Checking uncommitted file(s) after exclusions: ",
error_label: "Error getting uncommitted files",
},
)
} else if options.paths.is_empty() {
PathCollectionResult::Success(vec![PathBuf::from(".")], exclude_patterns)
} else {
PathCollectionResult::Success(options.paths.clone(), exclude_patterns)
}
}