Skip to content

Commit

Permalink
Implement meta hooks (#135)
Browse files Browse the repository at this point in the history
* temp

* temp

* Split run module

* Finish

* Fix clippy
  • Loading branch information
j178 authored Dec 9, 2024
1 parent 127d8ce commit 5b89765
Show file tree
Hide file tree
Showing 15 changed files with 1,053 additions and 691 deletions.
149 changes: 149 additions & 0 deletions src/builtin/meta_hooks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use std::collections::HashMap;
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;
use crate::hook::{Hook, Project};
use crate::store::Store;

/// Ensures that the configured hooks apply to at least one file in the repository.
pub async fn check_hooks_apply(
_hook: &Hook,
filenames: &[&String],
_env_vars: Arc<HashMap<&'static str, String>>,
) -> Result<(i32, Vec<u8>)> {
let store = Store::from_settings()?.init()?;

let input = get_filenames(FileOptions::default().with_all_files(true)).await?;

let mut code = 0;
let mut output = Vec::new();

for filename in filenames {
let mut project = Project::from_config_file(Some(PathBuf::from(filename)))?;
let hooks = project.init_hooks(&store, None).await?;

let filter = FileFilter::new(
&input,
project.config().files.as_deref(),
project.config().exclude.as_deref(),
)?;

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

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

if filenames.is_empty() {
code = 1;
output
.extend(format!("{} does not apply to this repository\n", hook.id).as_bytes());
}
}
}

Ok((code, output))
}

// 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);
}

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.
pub async fn check_useless_excludes(
_hook: &Hook,
filenames: &[&String],
_env_vars: Arc<HashMap<&'static str, String>>,
) -> Result<(i32, Vec<u8>)> {
let store = Store::from_settings()?.init()?;

let input = get_filenames(FileOptions::default().with_all_files(true)).await?;

let mut code = 0;
let mut output = Vec::new();

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(
&input,
project.config().files.as_deref(),
project.config().exclude.as_deref(),
)?;

for hook in hooks {
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!(
"The exclude pattern {:?} for {} does not match any files\n",
hook.exclude.as_deref().unwrap_or(""),
hook.id
)
.as_bytes(),
);
}
}
}

Ok((code, output))
}

/// Prints all arguments passed to the hook. Useful for debugging.
pub fn identity(
_hook: &Hook,
filenames: &[&String],
_env_vars: Arc<HashMap<&'static str, String>>,
) -> (i32, Vec<u8>) {
(0, filenames.iter().join("\n").into_bytes())
}
40 changes: 40 additions & 0 deletions src/builtin/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use crate::hook::{Hook, Repo};
use std::collections::HashMap;
use std::sync::Arc;

mod meta_hooks;

/// Returns true if the hook has a builtin Rust implementation.
pub fn check_fast_path(hook: &Hook) -> bool {
if matches!(hook.repo(), Repo::Meta { .. }) {
return true;
};

false
}

pub async fn run_fast_path(
hook: &Hook,
filenames: &[&String],
env_vars: Arc<HashMap<&'static str, String>>,
) -> anyhow::Result<(i32, Vec<u8>)> {
match hook.repo() {
Repo::Meta { .. } => run_meta_hook(hook, filenames, env_vars).await,
_ => unreachable!(),
}
}

async fn run_meta_hook(
hook: &Hook,
filenames: &[&String],
env_vars: Arc<HashMap<&'static str, String>>,
) -> anyhow::Result<(i32, Vec<u8>)> {
match hook.id.as_str() {
"check-hooks-apply" => meta_hooks::check_hooks_apply(hook, filenames, env_vars).await,
"check-useless-excludes" => {
meta_hooks::check_useless_excludes(hook, filenames, env_vars).await
}
"identity" => Ok(meta_hooks::identity(hook, filenames, env_vars)),
_ => unreachable!(),
}
}
2 changes: 1 addition & 1 deletion src/cli/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ pub(crate) async fn install(
let _lock = store.lock_async().await?;

let reporter = HookInitReporter::from(printer);
let hooks = project.init_hooks(&store, &reporter).await?;
let hooks = project.init_hooks(&store, Some(&reporter)).await?;
let reporter = HookInstallReporter::from(printer);
run::install_hooks(&hooks, &reporter).await?;
}
Expand Down
2 changes: 1 addition & 1 deletion src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ mod clean;
mod hook_impl;
mod install;
mod reporter;
mod run;
pub mod run;
mod sample_config;
mod self_update;
mod validate;
Expand Down
Loading

0 comments on commit 5b89765

Please sign in to comment.