Skip to content

Commit

Permalink
Finish
Browse files Browse the repository at this point in the history
  • Loading branch information
j178 committed Dec 9, 2024
1 parent 5fd9e55 commit 77fd211
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 19 deletions.
62 changes: 50 additions & 12 deletions src/builtin/meta_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use std::path::PathBuf;
use std::sync::Arc;

use anyhow::Result;
use fancy_regex::Regex;
use itertools::Itertools;
use rayon::iter::{IntoParallelIterator, ParallelIterator};

use crate::cli::run::{get_filenames, FileFilter, FileOptions};
use crate::config::Language;
Expand Down Expand Up @@ -51,12 +53,32 @@ pub async fn check_hooks_apply(
Ok((code, output))
}

fn excludes_any(files: &[String], include: &str, exclude: &str) -> bool {
if exclude == "^$" {
return true;
// Returns true if the exclude patter matches any files matching the include pattern.
fn excludes_any<T: AsRef<str> + Sync>(
files: &[T],
include: Option<&str>,
exclude: Option<&str>,
) -> Result<bool> {
if exclude.is_none_or(|s| s == "^$") {
return Ok(true);
}

true
let include = include.map(Regex::new).transpose()?;
let exclude = exclude.map(Regex::new).transpose()?;
Ok(files.into_par_iter().any(|f| {
let f = f.as_ref();
if let Some(re) = &include {
if !re.is_match(f).unwrap_or(false) {
return false;
}
}
if let Some(re) = &exclude {
if !re.is_match(f).unwrap_or(false) {
return false;
}
}
true
}))
}

/// Ensures that exclude directives apply to any file in the repository.
Expand All @@ -74,6 +96,18 @@ pub async fn check_useless_excludes(

for filename in filenames {
let mut project = Project::from_config_file(Some(PathBuf::from(filename)))?;

if !excludes_any(&input, None, project.config().exclude.as_deref())? {
code = 1;
output.extend(
format!(
"The global exclude pattern {:?} does not match any files",
project.config().exclude.as_deref().unwrap_or("")
)
.as_bytes(),
);
}

let hooks = project.init_hooks(&store, None).await?;

let filter = FileFilter::new(
Expand All @@ -83,16 +117,20 @@ pub async fn check_useless_excludes(
)?;

for hook in hooks {
if hook.always_run || matches!(hook.language, Language::Fail) {
continue;
}

let filenames = filter.for_hook(&hook)?;

if filenames.len() == input.len() {
let filtered_files = filter.by_tag(&hook);
if !excludes_any(
&filtered_files,
hook.files.as_deref(),
hook.exclude.as_deref(),
)? {
code = 1;
output.extend(
format!("{} excludes all files in the repository\n", hook.id).as_bytes(),
format!(
"The exclude pattern {:?} for {} does not match any files\n",
hook.exclude.as_deref().unwrap_or(""),
hook.id
)
.as_bytes(),
);
}
}
Expand Down
23 changes: 22 additions & 1 deletion src/cli/run/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::hook::Hook;
use crate::identify::tags_from_path;

/// Filter filenames by include/exclude patterns.
struct FilenameFilter {
pub struct FilenameFilter {
include: Option<Regex>,
exclude: Option<Regex>,
}
Expand Down Expand Up @@ -114,6 +114,27 @@ impl<'a> FileFilter<'a> {
self.filenames.len()
}

pub fn by_tag(&self, hook: &Hook) -> Vec<&String> {
let filter = FileTagFilter::from_hook(hook);
let filenames: Vec<_> = self
.filenames
.par_iter()
.filter(|filename| {
let path = Path::new(filename);
match tags_from_path(path) {
Ok(tags) => filter.filter(&tags),
Err(err) => {
error!(filename, error = %err, "Failed to get tags");
false
}
}
})
.copied()
.collect();

filenames
}

pub fn for_hook(&self, hook: &Hook) -> Result<Vec<&String>, Box<regex::Error>> {
let filter = FilenameFilter::from_hook(hook)?;
let filenames = self
Expand Down
49 changes: 43 additions & 6 deletions tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,28 +184,65 @@ fn local_need_install() {
}

#[test]
fn meta_hooks() {
fn meta_hooks() -> Result<()> {
let context = TestContext::new();
context.init_project();

context.write_pre_commit_config(indoc::indoc! {r"
let cwd = context.workdir();
cwd.child("file.txt").write_str("Hello, world!\n")?;
cwd.child("valid.json").write_str("{}")?;
cwd.child("invalid.json").write_str("{}")?;
cwd.child("main.py").write_str(r#"print "abc" "#)?;

context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: meta
hooks:
- id: check-hooks-apply
- id: check-useless-excludes
- id: identity
"});
- repo: local
hooks:
- id: match-no-files
name: match no files
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
files: ^nonexistent$
- id: useless-exclude
name: useless exclude
language: system
entry: python3 -c 'import sys; sys.exit(0)'
exclude: $nonexistent^
"#});
context.git_add(".");

cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
success: false
exit_code: 1
----- stdout -----
local....................................................................Passed
Check hooks apply........................................................Failed
- hook id: check-hooks-apply
- exit code: 1
match-no-files does not apply to this repository
Check useless excludes...................................................Failed
- hook id: check-useless-excludes
- exit code: 1
The exclude pattern "$nonexistent^" for useless-exclude does not match any files
identity.................................................................Passed
- hook id: identity
- duration: [TIME]
invalid.json
valid.json
main.py
.pre-commit-config.yaml
file.txt
match no files.......................................(no files to check)Skipped
useless exclude..........................................................Passed
----- stderr -----
"#);

Ok(())
}

#[test]
Expand Down

0 comments on commit 77fd211

Please sign in to comment.