use std::path::Path;
use crate::{CodeLoreError, Options, Result};
pub struct PathsFilter {
globset: globset::GlobSet,
gitignore: ignore::gitignore::Gitignore,
}
impl PathsFilter {
pub fn from_opts(opts: &Options) -> Result<Self> {
let mut gb = ignore::gitignore::GitignoreBuilder::new(&opts.repo_path);
if !opts.include_ignored {
let gitignore_path = opts.repo_path.join(".gitignore");
if gitignore_path.is_file()
&& let Some(err) = gb.add(&gitignore_path)
{
return Err(CodeLoreError::Analysis(format!(
".gitignore at {}: {err}",
gitignore_path.display()
)));
}
let info_exclude = opts.repo_path.join(".git/info/exclude");
if info_exclude.is_file()
&& let Some(err) = gb.add(&info_exclude)
{
return Err(CodeLoreError::Analysis(format!(
".git/info/exclude at {}: {err}",
info_exclude.display()
)));
}
let codelore_ignore = opts.repo_path.join(".codeloreignore");
if codelore_ignore.is_file()
&& let Some(err) = gb.add(&codelore_ignore)
{
return Err(CodeLoreError::Analysis(format!(".codeloreignore: {err}")));
}
}
let gitignore = gb
.build()
.map_err(|e| CodeLoreError::Analysis(format!("build gitignore matcher: {e}")))?;
let mut gsb = globset::GlobSetBuilder::new();
for pat in &opts.exclude_patterns {
let g = globset::Glob::new(pat)
.map_err(|e| CodeLoreError::Analysis(format!("--exclude {pat:?}: {e}")))?;
gsb.add(g);
}
let globset = gsb
.build()
.map_err(|e| CodeLoreError::Analysis(format!("build --exclude globset: {e}")))?;
Ok(Self { globset, gitignore })
}
#[must_use]
pub fn is_excluded(&self, rel_path: &Path, is_dir: bool) -> bool {
if self.globset.is_match(rel_path) {
return true;
}
match self.gitignore.matched_path_or_any_parents(rel_path, is_dir) {
ignore::Match::Ignore(_) => true,
ignore::Match::Whitelist(_) | ignore::Match::None => false,
}
}
}
#[must_use]
pub fn is_git_metadata(rel_path: &Path) -> bool {
rel_path
.components()
.next()
.and_then(|c| c.as_os_str().to_str())
== Some(".git")
}
#[cfg(all(test, feature = "test-support"))]
mod tests {
use std::path::Path;
use super::{PathsFilter, is_git_metadata};
use crate::Options;
fn filter_with(
files: &[(&str, &str)],
exclude: &[&str],
include_ignored: bool,
) -> (tempfile::TempDir, PathsFilter) {
let dir = tempfile::tempdir().expect("tempdir");
for (rel, contents) in files {
let path = dir.path().join(rel);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("mkdir");
}
std::fs::write(&path, contents).expect("write ignore file");
}
let opts = Options {
repo_path: dir.path().to_path_buf(),
exclude_patterns: exclude.iter().map(ToString::to_string).collect(),
include_ignored,
..Options::default()
};
let filter = PathsFilter::from_opts(&opts).expect("build filter");
(dir, filter)
}
#[test]
fn gitignore_rules_apply_including_negation_and_ancestors() {
let (_dir, f) = filter_with(&[(".gitignore", "dist/\n*.log\n!keep.log\n")], &[], false);
assert!(f.is_excluded(Path::new("dist/bundle.js"), false));
assert!(f.is_excluded(Path::new("debug.log"), false));
assert!(!f.is_excluded(Path::new("keep.log"), false));
assert!(!f.is_excluded(Path::new("src/main.rs"), false));
}
#[test]
fn git_info_exclude_applies_like_gitignore() {
let (_dir, f) = filter_with(&[(".git/info/exclude", "scratch.txt\n")], &[], false);
assert!(f.is_excluded(Path::new("scratch.txt"), false));
assert!(!f.is_excluded(Path::new("src/lib.rs"), false));
}
#[test]
fn codeloreignore_extends_the_gitignore_set() {
let (_dir, f) = filter_with(
&[(".gitignore", "dist/\n"), (".codeloreignore", "locales/\n")],
&[],
false,
);
assert!(f.is_excluded(Path::new("dist/x.js"), false));
assert!(f.is_excluded(Path::new("locales/de.json"), false));
assert!(!f.is_excluded(Path::new("src/lib.rs"), false));
}
#[test]
fn include_ignored_disables_the_ignore_family_but_not_exclude_globs() {
let (_dir, f) = filter_with(
&[(".gitignore", "dist/\n"), (".codeloreignore", "locales/\n")],
&["**/*.gen.rs"],
true,
);
assert!(!f.is_excluded(Path::new("dist/x.js"), false));
assert!(!f.is_excluded(Path::new("locales/de.json"), false));
assert!(f.is_excluded(Path::new("src/api.gen.rs"), false));
}
#[test]
fn exclude_globs_match_as_plain_globset() {
let (_dir, f) = filter_with(&[], &["**/*.min.js", "vendor/**"], false);
assert!(f.is_excluded(Path::new("assets/app.min.js"), false));
assert!(f.is_excluded(Path::new("vendor/lib/x.c"), false));
assert!(!f.is_excluded(Path::new("src/app.js"), false));
}
#[test]
fn git_metadata_is_only_the_top_level_git_directory() {
assert!(is_git_metadata(Path::new(".git/config")));
assert!(is_git_metadata(Path::new(".git/objects/ab/cdef")));
assert!(!is_git_metadata(Path::new(".gitignore")));
assert!(!is_git_metadata(Path::new("vendor/.git/config")));
}
}