Repository: j178/prefligit (language: python)

Issue:
Implement meta hooks



Discussion / hints:

Fix patch:
diff --git a/src/builtin/meta_hooks.rs b/src/builtin/meta_hooks.rs
new file mode 100644
index 0000000..27be1a3
--- /dev/null
+++ b/src/builtin/meta_hooks.rs
@@ -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())
+}
diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs
new file mode 100644
index 0000000..906ef78
--- /dev/null
+++ b/src/builtin/mod.rs
@@ -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!(),
+    }
+}
diff --git a/src/cli/install.rs b/src/cli/install.rs
index 0481718..e4fc109 100644
--- a/src/cli/install.rs
+++ b/src/cli/install.rs
@@ -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?;
     }
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
index bcb2484..15d8d76 100644
--- a/src/cli/mod.rs
+++ b/src/cli/mod.rs
@@ -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;
diff --git a/src/cli/run/filter.rs b/src/cli/run/filter.rs
new file mode 100644
index 0000000..ffb9df2
--- /dev/null
+++ b/src/cli/run/filter.rs
@@ -0,0 +1,259 @@
+use std::path::{Path, PathBuf};
+
+use anyhow::Result;
+use fancy_regex as regex;
+use fancy_regex::Regex;
+use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator};
+use tracing::{debug, error};
+
+use crate::config::Stage;
+use crate::fs::normalize_path;
+use crate::git;
+use crate::hook::Hook;
+use crate::identify::tags_from_path;
+
+/// Filter filenames by include/exclude patterns.
+pub struct FilenameFilter {
+    include: Option<Regex>,
+    exclude: Option<Regex>,
+}
+
+impl FilenameFilter {
+    pub fn new(include: Option<&str>, exclude: Option<&str>) -> Result<Self, Box<regex::Error>> {
+        let include = include.map(Regex::new).transpose()?;
+        let exclude = exclude.map(Regex::new).transpose()?;
+        Ok(Self { include, exclude })
+    }
+
+    pub fn filter(&self, filename: impl AsRef<str>) -> bool {
+        let filename = filename.as_ref();
+        if let Some(re) = &self.include {
+            if !re.is_match(filename).unwrap_or(false) {
+                return false;
+            }
+        }
+        if let Some(re) = &self.exclude {
+            if re.is_match(filename).unwrap_or(false) {
+                return false;
+            }
+        }
+        true
+    }
+
+    pub fn from_hook(hook: &Hook) -> Result<Self, Box<regex::Error>> {
+        Self::new(hook.files.as_deref(), hook.exclude.as_deref())
+    }
+}
+
+/// Filter files by tags.
+struct FileTagFilter<'a> {
+    all: &'a [String],
+    any: &'a [String],
+    exclude: &'a [String],
+}
+
+impl<'a> FileTagFilter<'a> {
+    fn new(types: &'a [String], types_or: &'a [String], exclude_types: &'a [String]) -> Self {
+        Self {
+            all: types,
+            any: types_or,
+            exclude: exclude_types,
+        }
+    }
+
+    fn filter(&self, file_types: &[&str]) -> bool {
+        if !self.all.is_empty() && !self.all.iter().all(|t| file_types.contains(&t.as_str())) {
+            return false;
+        }
+        if !self.any.is_empty() && !self.any.iter().any(|t| file_types.contains(&t.as_str())) {
+            return false;
+        }
+        if self
+            .exclude
+            .iter()
+            .any(|t| file_types.contains(&t.as_str()))
+        {
+            return false;
+        }
+        true
+    }
+
+    fn from_hook(hook: &'a Hook) -> Self {
+        Self::new(&hook.types, &hook.types_or, &hook.exclude_types)
+    }
+}
+
+pub struct FileFilter<'a> {
+    filenames: Vec<&'a String>,
+}
+
+impl<'a> FileFilter<'a> {
+    pub fn new(
+        filenames: &'a [String],
+        include: Option<&str>,
+        exclude: Option<&str>,
+    ) -> Result<Self, Box<regex::Error>> {
+        let filter = FilenameFilter::new(include, exclude)?;
+
+        let filenames = filenames
+            .into_par_iter()
+            .filter(|filename| filter.filter(filename))
+            .filter(|filename| {
+                // TODO: does this check really necessary?
+                // Ignore not existing files.
+                std::fs::symlink_metadata(filename)
+                    .map(|m| m.file_type().is_file())
+                    .unwrap_or(false)
+            })
+            .collect::<Vec<_>>();
+
+        Ok(Self { filenames })
+    }
+
+    pub fn len(&self) -> usize {
+        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
+            .filenames
+            .par_iter()
+            .filter(|filename| filter.filter(filename));
+
+        let filter = FileTagFilter::from_hook(hook);
+        let filenames: Vec<_> = filenames
+            .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();
+
+        Ok(filenames)
+    }
+}
+
+#[derive(Default)]
+pub struct FileOptions {
+    pub hook_stage: Option<Stage>,
+    pub from_ref: Option<String>,
+    pub to_ref: Option<String>,
+    pub all_files: bool,
+    pub files: Vec<PathBuf>,
+    pub commit_msg_filename: Option<PathBuf>,
+}
+
+impl FileOptions {
+    pub fn with_all_files(mut self, all_files: bool) -> Self {
+        self.all_files = all_files;
+        self
+    }
+}
+
+/// Get all filenames to run hooks on.
+#[allow(clippy::too_many_arguments)]
+pub async fn get_filenames(opts: FileOptions) -> Result<Vec<String>> {
+    let FileOptions {
+        hook_stage,
+        from_ref,
+        to_ref,
+        all_files,
+        files,
+        commit_msg_filename,
+    } = opts;
+
+    let mut filenames = filenames_for_args(
+        hook_stage,
+        from_ref,
+        to_ref,
+        all_files,
+        files,
+        commit_msg_filename,
+    )
+    .await?;
+
+    for filename in &mut filenames {
+        normalize_path(filename);
+    }
+    Ok(filenames)
+}
+
+#[allow(clippy::too_many_arguments)]
+async fn filenames_for_args(
+    hook_stage: Option<Stage>,
+    from_ref: Option<String>,
+    to_ref: Option<String>,
+    all_files: bool,
+    files: Vec<PathBuf>,
+    commit_msg_filename: Option<PathBuf>,
+) -> Result<Vec<String>> {
+    if hook_stage.is_some_and(|stage| !stage.operate_on_files()) {
+        return Ok(vec![]);
+    }
+    if hook_stage.is_some_and(|stage| matches!(stage, Stage::PrepareCommitMsg | Stage::CommitMsg)) {
+        return Ok(vec![commit_msg_filename
+            .unwrap()
+            .to_string_lossy()
+            .to_string()]);
+    }
+    if let (Some(from_ref), Some(to_ref)) = (from_ref, to_ref) {
+        let files = git::get_changed_files(&from_ref, &to_ref).await?;
+        debug!(
+            "Files changed between {} and {}: {}",
+            from_ref,
+            to_ref,
+            files.len()
+        );
+        return Ok(files);
+    }
+
+    if !files.is_empty() {
+        let files: Vec<_> = files
+            .into_iter()
+            .map(|f| f.to_string_lossy().to_string())
+            .collect();
+        debug!("Files passed as arguments: {}", files.len());
+        return Ok(files);
+    }
+    if all_files {
+        let files = git::get_all_files().await?;
+        debug!("All files in the repo: {}", files.len());
+        return Ok(files);
+    }
+    if git::is_in_merge_conflict().await? {
+        let files = git::get_conflicted_files().await?;
+        debug!("Conflicted files: {}", files.len());
+        return Ok(files);
+    }
+    let files = git::get_staged_files().await?;
+    debug!("Staged files: {}", files.len());
+    Ok(files)
+}
diff --git a/src/cli/run/keeper.rs b/src/cli/run/keeper.rs
new file mode 100644
index 0000000..22e76c2
--- /dev/null
+++ b/src/cli/run/keeper.rs
@@ -0,0 +1,249 @@
+use std::path::{Path, PathBuf};
+use std::process::Command;
+use std::sync::Mutex;
+
+use anstream::eprintln;
+use anyhow::Result;
+use owo_colors::OwoColorize;
+use tracing::{error, trace};
+
+use crate::cleanup::add_cleanup;
+use crate::fs::Simplified;
+use crate::git::{self, git_cmd, GIT};
+use crate::store::Store;
+
+static RESTORE_WORKTREE: Mutex<Option<WorkTreeKeeper>> = Mutex::new(None);
+
+struct IntentToAddKeeper(Vec<PathBuf>);
+struct WorkingTreeKeeper(Option<PathBuf>);
+
+impl IntentToAddKeeper {
+    async fn clean() -> Result<Self> {
+        let files = git::intent_to_add_files().await?;
+        if files.is_empty() {
+            return Ok(Self(vec![]));
+        }
+
+        // TODO: xargs
+        git_cmd("git rm")?
+            .arg("rm")
+            .arg("--cached")
+            .arg("--")
+            .args(&files)
+            .check(true)
+            .stdout(std::process::Stdio::null())
+            .stderr(std::process::Stdio::null())
+            .status()
+            .await?;
+
+        Ok(Self(files.into_iter().map(PathBuf::from).collect()))
+    }
+
+    fn restore(&self) -> Result<()> {
+        // Restore the intent-to-add changes.
+        if !self.0.is_empty() {
+            Command::new(GIT.as_ref()?)
+                .arg("add")
+                .arg("--intent-to-add")
+                .arg("--")
+                // TODO: xargs
+                .args(&self.0)
+                .stdout(std::process::Stdio::null())
+                .stderr(std::process::Stdio::null())
+                .status()?;
+        }
+        Ok(())
+    }
+}
+
+impl Drop for IntentToAddKeeper {
+    fn drop(&mut self) {
+        if let Err(err) = self.restore() {
+            anstream::eprintln!(
+                "{}",
+                format!("Failed to restore intent-to-add changes: {err}").red()
+            );
+        }
+    }
+}
+
+impl WorkingTreeKeeper {
+    async fn clean(patch_dir: &Path) -> Result<Self> {
+        let tree = git::write_tree().await?;
+
+        let mut cmd = git_cmd("git diff-index")?;
+        let output = cmd
+            .arg("diff-index")
+            .arg("--ignore-submodules")
+            .arg("--binary")
+            .arg("--exit-code")
+            .arg("--no-color")
+            .arg("--no-ext-diff")
+            .arg(tree)
+            .arg("--")
+            .check(false)
+            .output()
+            .await?;
+
+        if output.status.success() {
+            trace!("No non-staged changes detected");
+            // No non-staged changes
+            Ok(Self(None))
+        } else if output.status.code() == Some(1) {
+            if output.stdout.trim_ascii().is_empty() {
+                trace!("diff-index status code 1 with empty stdout");
+                // probably git auto crlf behavior quirks
+                Ok(Self(None))
+            } else {
+                let now = std::time::SystemTime::now();
+                let pid = std::process::id();
+                let patch_name = format!(
+                    "{}-{}.patch",
+                    now.duration_since(std::time::UNIX_EPOCH)?.as_millis(),
+                    pid
+                );
+                let patch_path = patch_dir.join(&patch_name);
+
+                anstream::eprintln!(
+                    "{}",
+                    format!(
+                        "Non-staged changes detected, saving to `{}`",
+                        patch_path.user_display()
+                    )
+                    .yellow()
+                );
+                fs_err::create_dir_all(patch_dir)?;
+                fs_err::write(&patch_path, output.stdout)?;
+
+                // Clean the working tree
+                Self::checkout_working_tree()?;
+
+                Ok(Self(Some(patch_path)))
+            }
+        } else {
+            Err(cmd.check_status(output.status).unwrap_err().into())
+        }
+    }
+
+    fn checkout_working_tree() -> Result<()> {
+        let status = Command::new(GIT.as_ref()?)
+            .arg("-c")
+            .arg("submodule.recurse=0")
+            .arg("checkout")
+            .arg("--")
+            .arg(".")
+            // prevent recursive post-checkout hooks
+            .env("_PRE_COMMIT_SKIP_POST_CHECKOUT", "1")
+            .stdout(std::process::Stdio::null())
+            .stderr(std::process::Stdio::null())
+            .status()?;
+        if status.success() {
+            Ok(())
+        } else {
+            Err(anyhow::anyhow!("Failed to checkout working tree"))
+        }
+    }
+
+    fn git_apply(patch: &Path) -> Result<()> {
+        let status = Command::new(GIT.as_ref()?)
+            .arg("apply")
+            .arg("--whitespace=nowarn")
+            .arg(patch)
+            .stdout(std::process::Stdio::null())
+            .stderr(std::process::Stdio::null())
+            .status()?;
+        if status.success() {
+            Ok(())
+        } else {
+            Err(anyhow::anyhow!("Failed to apply the patch"))
+        }
+    }
+
+    fn restore(&self) -> Result<()> {
+        let Some(patch) = self.0.as_ref() else {
+            return Ok(());
+        };
+
+        // Try to apply the patch
+        if Self::git_apply(patch).is_err() {
+            error!("Failed to apply the patch, rolling back changes");
+            eprintln!(
+                "{}",
+                "Failed to apply the patch, rolling back changes".red()
+            );
+
+            Self::checkout_working_tree()?;
+            Self::git_apply(patch)?;
+        };
+
+        eprintln!(
+            "{}",
+            format!(
+                "\nRestored working tree changes from `{}`",
+                patch.user_display()
+            )
+            .yellow()
+        );
+
+        Ok(())
+    }
+}
+
+impl Drop for WorkingTreeKeeper {
+    fn drop(&mut self) {
+        if let Err(err) = self.restore() {
+            eprintln!(
+                "{}",
+                format!("Failed to restore working tree changes: {err}").red()
+            );
+        }
+    }
+}
+
+/// Clean Git intent-to-add files and working tree changes, and restore them when dropped.
+pub struct WorkTreeKeeper {
+    intent_to_add: Option<IntentToAddKeeper>,
+    working_tree: Option<WorkingTreeKeeper>,
+}
+
+#[derive(Default)]
+pub struct RestoreGuard {
+    _guard: (),
+}
+
+impl Drop for RestoreGuard {
+    fn drop(&mut self) {
+        if let Some(mut keeper) = RESTORE_WORKTREE.lock().unwrap().take() {
+            keeper.restore();
+        }
+    }
+}
+
+impl WorkTreeKeeper {
+    /// Clear intent-to-add changes from the index and clear the non-staged changes from the working directory.
+    /// Restore them when the instance is dropped.
+    pub async fn clean(store: &Store) -> Result<RestoreGuard> {
+        let cleaner = Self {
+            intent_to_add: Some(IntentToAddKeeper::clean().await?),
+            working_tree: Some(WorkingTreeKeeper::clean(store.path()).await?),
+        };
+
+        // Set to the global for the cleanup hook.
+        *RESTORE_WORKTREE.lock().unwrap() = Some(cleaner);
+
+        // Make sure restoration when ctrl-c is pressed.
+        add_cleanup(|| {
+            if let Some(guard) = &mut *RESTORE_WORKTREE.lock().unwrap() {
+                guard.restore();
+            }
+        });
+
+        Ok(RestoreGuard::default())
+    }
+
+    /// Restore the intent-to-add changes and non-staged changes.
+    fn restore(&mut self) {
+        self.intent_to_add.take();
+        self.working_tree.take();
+    }
+}
diff --git a/src/cli/run/mod.rs b/src/cli/run/mod.rs
new file mode 100644
index 0000000..c3538e0
--- /dev/null
+++ b/src/cli/run/mod.rs
@@ -0,0 +1,7 @@
+pub use filter::{get_filenames, FileFilter, FileOptions};
+pub(crate) use run::{install_hooks, run};
+
+mod filter;
+mod keeper;
+#[allow(clippy::module_inception)]
+mod run;
diff --git a/src/cli/run.rs b/src/cli/run/run.rs
similarity index 54%
rename from src/cli/run.rs
rename to src/cli/run/run.rs
index 9ba0bf3..f1d721d 100644
--- a/src/cli/run.rs
+++ b/src/cli/run/run.rs
@@ -1,23 +1,31 @@
+use std::cmp::max;
 use std::collections::HashMap;
-use std::fmt::Write;
+use std::fmt::Write as _;
+use std::io::Write;
 use std::path::{Path, PathBuf};
+use std::sync::Arc;
 
+use anstream::ColorChoice;
 use anyhow::Result;
 use futures::stream::FuturesUnordered;
 use futures::StreamExt;
 use itertools::Itertools;
-use owo_colors::OwoColorize;
-use rayon::iter::{IntoParallelIterator, ParallelIterator};
+use owo_colors::{OwoColorize, Style};
+use rand::prelude::{SliceRandom, StdRng};
+use rand::SeedableRng;
 use tracing::{debug, trace};
+use unicode_width::UnicodeWidthStr;
 
 use crate::cli::reporter::{HookInitReporter, HookInstallReporter};
+use crate::cli::run::keeper::WorkTreeKeeper;
+use crate::cli::run::{get_filenames, FileFilter, FileOptions};
 use crate::cli::{ExitStatus, RunExtraArgs};
 use crate::config::Stage;
-use crate::fs::{normalize_path, Simplified};
+use crate::fs::Simplified;
 use crate::git;
+use crate::git::{get_diff, git_cmd};
 use crate::hook::{Hook, Project};
 use crate::printer::Printer;
-use crate::run::{run_hooks, FilenameFilter, WorkTreeKeeper};
 use crate::store::Store;
 
 #[allow(clippy::too_many_arguments)]
@@ -71,7 +79,7 @@ pub(crate) async fn run(
     let reporter = HookInitReporter::from(printer);
 
     let lock = store.lock_async().await?;
-    let hooks = project.init_hooks(&store, &reporter).await?;
+    let hooks = project.init_hooks(&store, Some(&reporter)).await?;
 
     let hooks: Vec<_> = hooks
         .into_iter()
@@ -130,40 +138,27 @@ pub(crate) async fn run(
         _guard = Some(WorkTreeKeeper::clean(&store).await?);
     }
 
-    let mut filenames = all_filenames(
+    let filenames = get_filenames(FileOptions {
         hook_stage,
         from_ref,
         to_ref,
         all_files,
         files,
-        extra_args.commit_msg_filename.as_ref(),
-    )
+        commit_msg_filename: extra_args.commit_msg_filename.clone(),
+    })
     .await?;
-    for filename in &mut filenames {
-        normalize_path(filename);
-    }
 
-    let filter = FilenameFilter::new(
+    let filter = FileFilter::new(
+        &filenames,
         project.config().files.as_deref(),
         project.config().exclude.as_deref(),
     )?;
-    let filenames = filenames
-        .into_par_iter()
-        .filter(|filename| filter.filter(filename))
-        .filter(|filename| {
-            // Ignore not existing files.
-            std::fs::symlink_metadata(filename)
-                .map(|m| m.file_type().is_file())
-                .unwrap_or(false)
-        })
-        .collect::<Vec<_>>();
-
-    trace!("Files after filtered: {}", filenames.len());
+    trace!("Files after filtered: {}", filter.len());
 
     run_hooks(
         &hooks,
         &skips,
-        filenames,
+        &filter,
         env_vars,
         project.config().fail_fast.unwrap_or(false),
         show_diff_on_failure,
@@ -253,59 +248,6 @@ fn get_skips() -> Vec<String> {
     }
 }
 
-/// Get all filenames to run hooks on.
-#[allow(clippy::too_many_arguments)]
-async fn all_filenames(
-    hook_stage: Option<Stage>,
-    from_ref: Option<String>,
-    to_ref: Option<String>,
-    all_files: bool,
-    files: Vec<PathBuf>,
-    commit_msg_filename: Option<&PathBuf>,
-) -> Result<Vec<String>> {
-    if hook_stage.is_some_and(|stage| !stage.operate_on_files()) {
-        return Ok(vec![]);
-    }
-    if hook_stage.is_some_and(|stage| matches!(stage, Stage::PrepareCommitMsg | Stage::CommitMsg)) {
-        return Ok(vec![commit_msg_filename
-            .unwrap()
-            .to_string_lossy()
-            .to_string()]);
-    }
-    if let (Some(from_ref), Some(to_ref)) = (from_ref, to_ref) {
-        let files = git::get_changed_files(&from_ref, &to_ref).await?;
-        debug!(
-            "Files changed between {} and {}: {}",
-            from_ref,
-            to_ref,
-            files.len()
-        );
-        return Ok(files);
-    }
-
-    if !files.is_empty() {
-        let files: Vec<_> = files
-            .into_iter()
-            .map(|f| f.to_string_lossy().to_string())
-            .collect();
-        debug!("Files passed as arguments: {}", files.len());
-        return Ok(files);
-    }
-    if all_files {
-        let files = git::get_all_files().await?;
-        debug!("All files in the repo: {}", files.len());
-        return Ok(files);
-    }
-    if git::is_in_merge_conflict().await? {
-        let files = git::get_conflicted_files().await?;
-        debug!("Conflicted files: {}", files.len());
-        return Ok(files);
-    }
-    let files = git::get_staged_files().await?;
-    debug!("Staged files: {}", files.len());
-    Ok(files)
-}
-
 async fn install_hook(hook: &Hook, env_dir: PathBuf) -> Result<()> {
     debug!(%hook, target = %env_dir.display(), "Install environment");
 
@@ -348,3 +290,221 @@ pub async fn install_hooks(hooks: &[Hook], reporter: &HookInstallReporter) -> Re
 
     Ok(())
 }
+
+const SKIPPED: &str = "Skipped";
+const NO_FILES: &str = "(no files to check)";
+
+fn status_line(start: &str, cols: usize, end_msg: &str, end_color: Style, postfix: &str) -> String {
+    let dots = cols - start.width_cjk() - end_msg.len() - postfix.len() - 1;
+    format!(
+        "{}{}{}{}",
+        start,
+        ".".repeat(dots),
+        postfix,
+        end_msg.style(end_color)
+    )
+}
+
+fn calculate_columns(hooks: &[Hook]) -> usize {
+    let name_len = hooks
+        .iter()
+        .map(|hook| hook.name.width_cjk())
+        .max()
+        .unwrap_or(0);
+    max(80, name_len + 3 + NO_FILES.len() + 1 + SKIPPED.len())
+}
+
+/// Run all hooks.
+pub async fn run_hooks(
+    hooks: &[Hook],
+    skips: &[String],
+    filter: &FileFilter<'_>,
+    env_vars: HashMap<&'static str, String>,
+    fail_fast: bool,
+    show_diff_on_failure: bool,
+    verbose: bool,
+    printer: Printer,
+) -> Result<ExitStatus> {
+    let env_vars = Arc::new(env_vars);
+
+    let columns = calculate_columns(hooks);
+    let mut success = true;
+
+    let mut diff = get_diff().await?;
+    // hooks must run in serial
+    for hook in hooks {
+        let (hook_success, new_diff) = run_hook(
+            hook,
+            filter,
+            env_vars.clone(),
+            skips,
+            diff,
+            columns,
+            verbose,
+            printer,
+        )
+        .await?;
+
+        success &= hook_success;
+        diff = new_diff;
+        if !success && (fail_fast || hook.fail_fast) {
+            break;
+        }
+    }
+
+    if !success && show_diff_on_failure {
+        writeln!(printer.stdout(), "All changes made by hooks:")?;
+        let color = match ColorChoice::global() {
+            ColorChoice::Auto => "--color=auto",
+            ColorChoice::Always | ColorChoice::AlwaysAnsi => "--color=always",
+            ColorChoice::Never => "--color=never",
+        };
+        git_cmd("git diff")?
+            .arg("--no-pager")
+            .arg("diff")
+            .arg("--no-ext-diff")
+            .arg(color)
+            .check(true)
+            .spawn()?
+            .wait()
+            .await?;
+    };
+
+    if success {
+        Ok(ExitStatus::Success)
+    } else {
+        Ok(ExitStatus::Failure)
+    }
+}
+
+/// Shuffle the files so that they more evenly fill out the xargs
+/// partitions, but do it deterministically in case a hook cares about ordering.
+fn shuffle<T>(filenames: &mut [T]) {
+    const SEED: u64 = 1_542_676_187;
+    let mut rng = StdRng::seed_from_u64(SEED);
+    filenames.shuffle(&mut rng);
+}
+
+async fn run_hook(
+    hook: &Hook,
+    filter: &FileFilter<'_>,
+    env_vars: Arc<HashMap<&'static str, String>>,
+    skips: &[String],
+    diff: Vec<u8>,
+    columns: usize,
+    verbose: bool,
+    printer: Printer,
+) -> Result<(bool, Vec<u8>)> {
+    if skips.contains(&hook.id) || skips.contains(&hook.alias) {
+        writeln!(
+            printer.stdout(),
+            "{}",
+            status_line(
+                &hook.name,
+                columns,
+                SKIPPED,
+                Style::new().black().on_yellow(),
+                "",
+            )
+        )?;
+        return Ok((true, diff));
+    }
+
+    let mut filenames = filter.for_hook(hook)?;
+
+    if filenames.is_empty() && !hook.always_run {
+        writeln!(
+            printer.stdout(),
+            "{}",
+            status_line(
+                &hook.name,
+                columns,
+                SKIPPED,
+                Style::new().black().on_cyan(),
+                NO_FILES,
+            )
+        )?;
+        return Ok((true, diff));
+    }
+
+    write!(
+        printer.stdout(),
+        "{}{}",
+        &hook.name,
+        ".".repeat(columns - hook.name.width_cjk() - 6 - 1)
+    )?;
+    std::io::stdout().flush()?;
+
+    let start = std::time::Instant::now();
+
+    let (status, output) = if hook.pass_filenames {
+        shuffle(&mut filenames);
+        hook.language.run(hook, &filenames, env_vars).await?
+    } else {
+        hook.language.run(hook, &[], env_vars).await?
+    };
+
+    let duration = start.elapsed();
+
+    let new_diff = get_diff().await?;
+    let file_modified = diff != new_diff;
+    let success = status == 0 && !file_modified;
+
+    if success {
+        writeln!(printer.stdout(), "{}", "Passed".on_green())?;
+    } else {
+        writeln!(printer.stdout(), "{}", "Failed".on_red())?;
+    }
+
+    if verbose || hook.verbose || !success {
+        writeln!(
+            printer.stdout(),
+            "{}",
+            format!("- hook id: {}", hook.id).dimmed()
+        )?;
+        if verbose || hook.verbose {
+            writeln!(
+                printer.stdout(),
+                "{}",
+                format!("- duration: {:.2?}s", duration.as_secs_f64()).dimmed()
+            )?;
+        }
+        if status != 0 {
+            writeln!(
+                printer.stdout(),
+                "{}",
+                format!("- exit code: {status}").dimmed()
+            )?;
+        }
+        if file_modified {
+            writeln!(
+                printer.stdout(),
+                "{}",
+                "- files were modified by this hook".dimmed()
+            )?;
+        }
+
+        // To be consistent with pre-commit, merge stderr into stdout.
+        let stdout = output.trim_ascii();
+        if !stdout.is_empty() {
+            if let Some(file) = hook.log_file.as_deref() {
+                fs_err::OpenOptions::new()
+                    .create(true)
+                    .append(true)
+                    .open(file)
+                    .and_then(|mut f| {
+                        f.write_all(stdout)?;
+                        Ok(())
+                    })?;
+            } else {
+                writeln!(
+                    printer.stdout(),
+                    "{}",
+                    textwrap::indent(&String::from_utf8_lossy(stdout), "  ").dimmed()
+                )?;
+            };
+        }
+    }
+
+    Ok((success, new_diff))
+}
diff --git a/src/config.rs b/src/config.rs
index bc7cfc9..0e081b8 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -127,7 +127,6 @@ impl Display for HookType {
     }
 }
 
-// TODO: warn on deprecated stages
 #[derive(Debug, Clone, Copy, PartialEq, Deserialize, clap::ValueEnum)]
 #[serde(rename_all = "kebab-case")]
 pub enum Stage {
@@ -265,7 +264,7 @@ impl RepoLocation {
         match self {
             RepoLocation::Local => "local",
             RepoLocation::Meta => "meta",
-            RepoLocation::Remote(_) => "remote",
+            RepoLocation::Remote(url) => url.as_str(),
         }
     }
 }
@@ -392,7 +391,7 @@ pub type ConfigLocalHook = ManifestHook;
 pub enum MetaHookID {
     CheckHooksApply,
     CheckUselessExcludes,
-    Identify,
+    Identity,
 }
 
 impl Display for MetaHookID {
@@ -400,7 +399,7 @@ impl Display for MetaHookID {
         let name = match self {
             MetaHookID::CheckHooksApply => "check-hooks-apply",
             MetaHookID::CheckUselessExcludes => "check-useless-excludes",
-            MetaHookID::Identify => "identify",
+            MetaHookID::Identity => "identity",
         };
         f.write_str(name)
     }
@@ -413,7 +412,7 @@ impl FromStr for MetaHookID {
         match s {
             "check-hooks-apply" => Ok(MetaHookID::CheckHooksApply),
             "check-useless-excludes" => Ok(MetaHookID::CheckUselessExcludes),
-            "identify" => Ok(MetaHookID::Identify),
+            "identity" => Ok(MetaHookID::Identity),
             _ => Err(()),
         }
     }
@@ -447,30 +446,30 @@ impl<'de> Deserialize<'de> for ConfigMetaHook {
 
         let mut defaults = match id {
             MetaHookID::CheckHooksApply => ManifestHook {
-                id: "check-hooks-apply".to_string(),
+                id: MetaHookID::CheckHooksApply.to_string(),
                 name: "Check hooks apply".to_string(),
                 language: Language::System,
-                entry: "a".to_string(), // TODO: direct call to the hook
+                entry: String::new(),
                 options: HookOptions {
                     files: Some(format!("^{}$", regex::escape(CONFIG_FILE))),
                     ..Default::default()
                 },
             },
             MetaHookID::CheckUselessExcludes => ManifestHook {
-                id: "check-useless-excludes".to_string(),
+                id: MetaHookID::CheckUselessExcludes.to_string(),
                 name: "Check useless excludes".to_string(),
                 language: Language::System,
-                entry: "a".to_string(),
+                entry: String::new(),
                 options: HookOptions {
                     files: Some(format!("^{}$", regex::escape(CONFIG_FILE))),
                     ..Default::default()
                 },
             },
-            MetaHookID::Identify => ManifestHook {
-                id: "identify".to_string(),
-                name: "identify".to_string(),
+            MetaHookID::Identity => ManifestHook {
+                id: MetaHookID::Identity.to_string(),
+                name: "identity".to_string(),
                 language: Language::System,
-                entry: "a".to_string(),
+                entry: String::new(),
                 options: HookOptions {
                     verbose: Some(true),
                     ..Default::default()
@@ -520,7 +519,6 @@ impl Display for ConfigRemoteRepo {
 
 #[derive(Debug, Clone)]
 pub struct ConfigLocalRepo {
-    pub repo: String,
     pub hooks: Vec<ConfigLocalHook>,
 }
 
@@ -532,7 +530,6 @@ impl Display for ConfigLocalRepo {
 
 #[derive(Debug, Clone)]
 pub struct ConfigMetaRepo {
-    pub repo: String,
     pub hooks: Vec<ConfigMetaHook>,
 }
 
@@ -587,10 +584,7 @@ impl<'de> Deserialize<'de> for ConfigRepo {
                 }
                 let LocalRepo { hooks } = LocalRepo::deserialize(rest)
                     .map_err(|e| serde::de::Error::custom(format!("Invalid local repo: {e}")))?;
-                Ok(ConfigRepo::Local(ConfigLocalRepo {
-                    repo: "local".to_string(),
-                    hooks,
-                }))
+                Ok(ConfigRepo::Local(ConfigLocalRepo { hooks }))
             }
             RepoLocation::Meta => {
                 #[derive(Deserialize)]
@@ -600,17 +594,12 @@ impl<'de> Deserialize<'de> for ConfigRepo {
                 }
                 let MetaRepo { hooks } = MetaRepo::deserialize(rest)
                     .map_err(|e| serde::de::Error::custom(format!("Invalid meta repo: {e}")))?;
-                Ok(ConfigRepo::Meta(ConfigMetaRepo {
-                    repo: "meta".to_string(),
-                    hooks,
-                }))
+                Ok(ConfigRepo::Meta(ConfigMetaRepo { hooks }))
             }
         }
     }
 }
 
-// TODO: check minimum_pre_commit_version
-
 #[derive(Debug, Clone, Deserialize)]
 #[serde(rename_all = "snake_case")]
 pub struct ManifestHook {
@@ -694,7 +683,6 @@ mod tests {
                 repos: [
                     Local(
                         ConfigLocalRepo {
-                            repo: "local",
                             hooks: [
                                 ManifestHook {
                                     id: "cargo-fmt",
@@ -895,7 +883,6 @@ mod tests {
                 repos: [
                     Local(
                         ConfigLocalRepo {
-                            repo: "local",
                             hooks: [
                                 ManifestHook {
                                     id: "cargo-fmt",
@@ -1009,7 +996,7 @@ mod tests {
                 hooks:
                   - id: check-hooks-apply
                   - id: check-useless-excludes
-                  - id: identify
+                  - id: identity
         "};
         let result = serde_yaml::from_str::<ConfigWire>(yaml);
         insta::assert_debug_snapshot!(result, @r###"
@@ -1018,13 +1005,12 @@ mod tests {
                 repos: [
                     Meta(
                         ConfigMetaRepo {
-                            repo: "meta",
                             hooks: [
                                 ConfigMetaHook(
                                     ManifestHook {
                                         id: "check-hooks-apply",
                                         name: "Check hooks apply",
-                                        entry: "a",
+                                        entry: "",
                                         language: System,
                                         options: HookOptions {
                                             alias: None,
@@ -1054,7 +1040,7 @@ mod tests {
                                     ManifestHook {
                                         id: "check-useless-excludes",
                                         name: "Check useless excludes",
-                                        entry: "a",
+                                        entry: "",
                                         language: System,
                                         options: HookOptions {
                                             alias: None,
@@ -1082,9 +1068,9 @@ mod tests {
                                 ),
                                 ConfigMetaHook(
                                     ManifestHook {
-                                        id: "identify",
-                                        name: "identify",
-                                        entry: "a",
+                                        id: "identity",
+                                        name: "identity",
+                                        entry: "",
                                         language: System,
                                         options: HookOptions {
                                             alias: None,
diff --git a/src/hook.rs b/src/hook.rs
index 9ad5ebb..632d508 100644
--- a/src/hook.rs
+++ b/src/hook.rs
@@ -160,7 +160,7 @@ impl Project {
     async fn init_repos(
         &mut self,
         store: &Store,
-        reporter: &dyn HookInitReporter,
+        reporter: Option<&dyn HookInitReporter>,
     ) -> Result<(), Error> {
         let mut tasks = FuturesUnordered::new();
         let mut seen = HashSet::new();
@@ -170,9 +170,14 @@ impl Project {
                     continue;
                 }
                 tasks.push(async move {
-                    let progress = reporter.on_clone_start(&format!("{repo}"));
+                    let progress = reporter
+                        .map(|reporter| (reporter, reporter.on_clone_start(&format!("{repo}"))));
+
                     let path = store.prepare_remote_repo(repo, &[]).await;
-                    reporter.on_clone_complete(progress);
+
+                    if let Some((reporter, progress)) = progress {
+                        reporter.on_clone_complete(progress);
+                    }
 
                     (repo, path)
                 });
@@ -217,7 +222,7 @@ impl Project {
     pub async fn init_hooks(
         &mut self,
         store: &Store,
-        reporter: &dyn HookInitReporter,
+        reporter: Option<&dyn HookInitReporter>,
     ) -> Result<Vec<Hook>, Error> {
         self.init_repos(store, reporter).await?;
 
@@ -296,7 +301,7 @@ impl Project {
             }
         }
 
-        reporter.on_complete();
+        reporter.map(HookInitReporter::on_complete);
 
         Ok(hooks)
     }
diff --git a/src/languages/mod.rs b/src/languages/mod.rs
index ddb2075..4517556 100644
--- a/src/languages/mod.rs
+++ b/src/languages/mod.rs
@@ -1,10 +1,10 @@
 use std::collections::HashMap;
 use std::sync::Arc;
 
-use anyhow::Result;
-
+use crate::builtin;
 use crate::config::Language;
 use crate::hook::Hook;
+use anyhow::Result;
 
 mod docker;
 mod docker_image;
@@ -90,6 +90,11 @@ impl Language {
         filenames: &[&String],
         env_vars: Arc<HashMap<&'static str, String>>,
     ) -> Result<(i32, Vec<u8>)> {
+        // fast path for hooks implemented in Rust
+        if builtin::check_fast_path(hook) {
+            return builtin::run_fast_path(hook, filenames, env_vars).await;
+        }
+
         match self {
             Self::Python => PYTHON.run(hook, filenames, env_vars).await,
             Self::Node => NODE.run(hook, filenames, env_vars).await,
diff --git a/src/main.rs b/src/main.rs
index a76ba95..1b01c07 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -16,6 +16,7 @@ use crate::cli::{Cli, Command, ExitStatus, SelfCommand, SelfNamespace, SelfUpdat
 use crate::git::get_root;
 use crate::printer::Printer;
 
+mod builtin;
 mod cleanup;
 mod cli;
 mod config;
diff --git a/src/run.rs b/src/run.rs
index ae99059..a808c5f 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -1,338 +1,11 @@
 use std::cmp::max;
-use std::collections::HashMap;
-use std::fmt::Write as _;
 use std::future::Future;
-use std::io::Write as _;
-use std::path::{Path, PathBuf};
-use std::process::Command;
-use std::sync::{Arc, Mutex};
+use std::sync::Arc;
 
-use anstream::{eprintln, ColorChoice};
-use anyhow::Result;
-use fancy_regex::{self as regex, Regex};
-use owo_colors::{OwoColorize, Style};
-use rand::prelude::{SliceRandom, StdRng};
-use rand::SeedableRng;
-use rayon::iter::{IntoParallelIterator, ParallelIterator};
 use tokio::task::JoinSet;
-use tracing::{error, trace};
-use unicode_width::UnicodeWidthStr;
+use tracing::trace;
 
-use crate::cleanup::add_cleanup;
-use crate::cli::ExitStatus;
-use crate::fs::Simplified;
-use crate::git;
-use crate::git::{get_diff, git_cmd, GIT};
 use crate::hook::Hook;
-use crate::identify::tags_from_path;
-use crate::printer::Printer;
-use crate::store::Store;
-
-const SKIPPED: &str = "Skipped";
-const NO_FILES: &str = "(no files to check)";
-
-/// Filter filenames by include/exclude patterns.
-pub struct FilenameFilter {
-    include: Option<Regex>,
-    exclude: Option<Regex>,
-}
-
-impl FilenameFilter {
-    pub fn new(include: Option<&str>, exclude: Option<&str>) -> Result<Self, Box<regex::Error>> {
-        let include = include.map(Regex::new).transpose()?;
-        let exclude = exclude.map(Regex::new).transpose()?;
-        Ok(Self { include, exclude })
-    }
-
-    pub fn filter(&self, filename: impl AsRef<str>) -> bool {
-        let filename = filename.as_ref();
-        if let Some(re) = &self.include {
-            if !re.is_match(filename).unwrap_or(false) {
-                return false;
-            }
-        }
-        if let Some(re) = &self.exclude {
-            if re.is_match(filename).unwrap_or(false) {
-                return false;
-            }
-        }
-        true
-    }
-
-    pub fn from_hook(hook: &Hook) -> Result<Self, Box<regex::Error>> {
-        Self::new(hook.files.as_deref(), hook.exclude.as_deref())
-    }
-}
-
-/// Filter files by tags.
-struct FileTagFilter<'a> {
-    all: &'a [String],
-    any: &'a [String],
-    exclude: &'a [String],
-}
-
-impl<'a> FileTagFilter<'a> {
-    fn new(types: &'a [String], types_or: &'a [String], exclude_types: &'a [String]) -> Self {
-        Self {
-            all: types,
-            any: types_or,
-            exclude: exclude_types,
-        }
-    }
-
-    fn filter(&self, file_types: &[&str]) -> bool {
-        if !self.all.is_empty() && !self.all.iter().all(|t| file_types.contains(&t.as_str())) {
-            return false;
-        }
-        if !self.any.is_empty() && !self.any.iter().any(|t| file_types.contains(&t.as_str())) {
-            return false;
-        }
-        if self
-            .exclude
-            .iter()
-            .any(|t| file_types.contains(&t.as_str()))
-        {
-            return false;
-        }
-        true
-    }
-
-    fn from_hook(hook: &'a Hook) -> Self {
-        Self::new(&hook.types, &hook.types_or, &hook.exclude_types)
-    }
-}
-
-fn status_line(start: &str, cols: usize, end_msg: &str, end_color: Style, postfix: &str) -> String {
-    let dots = cols - start.width_cjk() - end_msg.len() - postfix.len() - 1;
-    format!(
-        "{}{}{}{}",
-        start,
-        ".".repeat(dots),
-        postfix,
-        end_msg.style(end_color)
-    )
-}
-
-fn calculate_columns(hooks: &[Hook]) -> usize {
-    let name_len = hooks
-        .iter()
-        .map(|hook| hook.name.width_cjk())
-        .max()
-        .unwrap_or(0);
-    max(80, name_len + 3 + NO_FILES.len() + 1 + SKIPPED.len())
-}
-
-/// Run all hooks.
-pub async fn run_hooks(
-    hooks: &[Hook],
-    skips: &[String],
-    filenames: Vec<String>,
-    env_vars: HashMap<&'static str, String>,
-    fail_fast: bool,
-    show_diff_on_failure: bool,
-    verbose: bool,
-    printer: Printer,
-) -> Result<ExitStatus> {
-    let env_vars = Arc::new(env_vars);
-
-    let columns = calculate_columns(hooks);
-    let mut success = true;
-
-    let mut diff = get_diff().await?;
-    // hooks must run in serial
-    for hook in hooks {
-        let (hook_success, new_diff) = run_hook(
-            hook,
-            &filenames,
-            env_vars.clone(),
-            skips,
-            diff,
-            columns,
-            verbose,
-            printer,
-        )
-        .await?;
-
-        success &= hook_success;
-        diff = new_diff;
-        if !success && (fail_fast || hook.fail_fast) {
-            break;
-        }
-    }
-
-    if !success && show_diff_on_failure {
-        writeln!(printer.stdout(), "All changes made by hooks:")?;
-        let color = match ColorChoice::global() {
-            ColorChoice::Auto => "--color=auto",
-            ColorChoice::Always | ColorChoice::AlwaysAnsi => "--color=always",
-            ColorChoice::Never => "--color=never",
-        };
-        git_cmd("git diff")?
-            .arg("--no-pager")
-            .arg("diff")
-            .arg("--no-ext-diff")
-            .arg(color)
-            .check(true)
-            .spawn()?
-            .wait()
-            .await?;
-    };
-
-    if success {
-        Ok(ExitStatus::Success)
-    } else {
-        Ok(ExitStatus::Failure)
-    }
-}
-
-/// Shuffle the files so that they more evenly fill out the xargs
-/// partitions, but do it deterministically in case a hook cares about ordering.
-fn shuffle<T>(filenames: &mut [T]) {
-    const SEED: u64 = 1_542_676_187;
-    let mut rng = StdRng::seed_from_u64(SEED);
-    filenames.shuffle(&mut rng);
-}
-
-async fn run_hook(
-    hook: &Hook,
-    filenames: &[String],
-    env_vars: Arc<HashMap<&'static str, String>>,
-    skips: &[String],
-    diff: Vec<u8>,
-    columns: usize,
-    verbose: bool,
-    printer: Printer,
-) -> Result<(bool, Vec<u8>)> {
-    if skips.contains(&hook.id) || skips.contains(&hook.alias) {
-        writeln!(
-            printer.stdout(),
-            "{}",
-            status_line(
-                &hook.name,
-                columns,
-                SKIPPED,
-                Style::new().black().on_yellow(),
-                "",
-            )
-        )?;
-        return Ok((true, diff));
-    }
-
-    let filter = FilenameFilter::from_hook(hook)?;
-    let filenames = filenames
-        .into_par_iter()
-        .filter(|filename| filter.filter(filename));
-
-    let filter = FileTagFilter::from_hook(hook);
-    let mut filenames: Vec<_> = filenames
-        .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
-                }
-            }
-        })
-        .collect();
-
-    if filenames.is_empty() && !hook.always_run {
-        writeln!(
-            printer.stdout(),
-            "{}",
-            status_line(
-                &hook.name,
-                columns,
-                SKIPPED,
-                Style::new().black().on_cyan(),
-                NO_FILES,
-            )
-        )?;
-        return Ok((true, diff));
-    }
-
-    write!(
-        printer.stdout(),
-        "{}{}",
-        &hook.name,
-        ".".repeat(columns - hook.name.width_cjk() - 6 - 1)
-    )?;
-    std::io::stdout().flush()?;
-
-    let start = std::time::Instant::now();
-
-    let (status, output) = if hook.pass_filenames {
-        shuffle(&mut filenames);
-        hook.language.run(hook, &filenames, env_vars).await?
-    } else {
-        hook.language.run(hook, &[], env_vars).await?
-    };
-
-    let duration = start.elapsed();
-
-    let new_diff = get_diff().await?;
-    let file_modified = diff != new_diff;
-    let success = status == 0 && !file_modified;
-
-    if success {
-        writeln!(printer.stdout(), "{}", "Passed".on_green())?;
-    } else {
-        writeln!(printer.stdout(), "{}", "Failed".on_red())?;
-    }
-
-    if verbose || hook.verbose || !success {
-        writeln!(
-            printer.stdout(),
-            "{}",
-            format!("- hook id: {}", hook.id).dimmed()
-        )?;
-        if verbose || hook.verbose {
-            writeln!(
-                printer.stdout(),
-                "{}",
-                format!("- duration: {:.2?}s", duration.as_secs_f64()).dimmed()
-            )?;
-        }
-        if status != 0 {
-            writeln!(
-                printer.stdout(),
-                "{}",
-                format!("- exit code: {status}").dimmed()
-            )?;
-        }
-        if file_modified {
-            writeln!(
-                printer.stdout(),
-                "{}",
-                "- files were modified by this hook".dimmed()
-            )?;
-        }
-
-        // To be consistent with pre-commit, merge stderr into stdout.
-        let stdout = output.trim_ascii();
-        if !stdout.is_empty() {
-            if let Some(file) = hook.log_file.as_deref() {
-                fs_err::OpenOptions::new()
-                    .create(true)
-                    .append(true)
-                    .open(file)
-                    .and_then(|mut f| {
-                        f.write_all(stdout)?;
-                        Ok(())
-                    })?;
-            } else {
-                writeln!(
-                    printer.stdout(),
-                    "{}",
-                    textwrap::indent(&String::from_utf8_lossy(stdout), "  ").dimmed()
-                )?;
-            };
-        }
-    }
-
-    Ok((success, new_diff))
-}
 
 fn target_concurrency(serial: bool) -> usize {
     if serial || std::env::var_os("PRE_COMMIT_NO_CONCURRENCY").is_some() {
@@ -388,11 +61,15 @@ fn partitions<'a>(
     partitions
 }
 
-pub async fn run_by_batch<T, F, Fut>(hook: &Hook, filenames: &[&String], run: F) -> Result<Vec<T>>
+pub async fn run_by_batch<T, F, Fut>(
+    hook: &Hook,
+    filenames: &[&String],
+    run: F,
+) -> anyhow::Result<Vec<T>>
 where
     F: Fn(Vec<String>) -> Fut,
     F: Clone + Send + Sync + 'static,
-    Fut: Future<Output = Result<T>> + Send + 'static,
+    Fut: Future<Output = anyhow::Result<T>> + Send + 'static,
     T: Send + 'static,
 {
     let mut concurrency = target_concurrency(hook.require_serial);
@@ -437,239 +114,3 @@ where
 
     Ok(results)
 }
-
-static RESTORE_WORKTREE: Mutex<Option<WorkTreeKeeper>> = Mutex::new(None);
-
-struct IntentToAddKeeper(Vec<PathBuf>);
-struct WorkingTreeKeeper(Option<PathBuf>);
-
-impl IntentToAddKeeper {
-    async fn clean() -> Result<Self> {
-        let files = git::intent_to_add_files().await?;
-        if files.is_empty() {
-            return Ok(Self(vec![]));
-        }
-
-        // TODO: xargs
-        git_cmd("git rm")?
-            .arg("rm")
-            .arg("--cached")
-            .arg("--")
-            .args(&files)
-            .check(true)
-            .stdout(std::process::Stdio::null())
-            .stderr(std::process::Stdio::null())
-            .status()
-            .await?;
-
-        Ok(Self(files.into_iter().map(PathBuf::from).collect()))
-    }
-
-    fn restore(&self) -> Result<()> {
-        // Restore the intent-to-add changes.
-        if !self.0.is_empty() {
-            Command::new(GIT.as_ref()?)
-                .arg("add")
-                .arg("--intent-to-add")
-                .arg("--")
-                // TODO: xargs
-                .args(&self.0)
-                .stdout(std::process::Stdio::null())
-                .stderr(std::process::Stdio::null())
-                .status()?;
-        }
-        Ok(())
-    }
-}
-
-impl Drop for IntentToAddKeeper {
-    fn drop(&mut self) {
-        if let Err(err) = self.restore() {
-            eprintln!(
-                "{}",
-                format!("Failed to restore intent-to-add changes: {err}").red()
-            );
-        }
-    }
-}
-
-impl WorkingTreeKeeper {
-    async fn clean(patch_dir: &Path) -> Result<Self> {
-        let tree = git::write_tree().await?;
-
-        let mut cmd = git_cmd("git diff-index")?;
-        let output = cmd
-            .arg("diff-index")
-            .arg("--ignore-submodules")
-            .arg("--binary")
-            .arg("--exit-code")
-            .arg("--no-color")
-            .arg("--no-ext-diff")
-            .arg(tree)
-            .arg("--")
-            .check(false)
-            .output()
-            .await?;
-
-        if output.status.success() {
-            trace!("No non-staged changes detected");
-            // No non-staged changes
-            Ok(Self(None))
-        } else if output.status.code() == Some(1) {
-            if output.stdout.trim_ascii().is_empty() {
-                trace!("diff-index status code 1 with empty stdout");
-                // probably git auto crlf behavior quirks
-                Ok(Self(None))
-            } else {
-                let now = std::time::SystemTime::now();
-                let pid = std::process::id();
-                let patch_name = format!(
-                    "{}-{}.patch",
-                    now.duration_since(std::time::UNIX_EPOCH)?.as_millis(),
-                    pid
-                );
-                let patch_path = patch_dir.join(&patch_name);
-
-                eprintln!(
-                    "{}",
-                    format!(
-                        "Non-staged changes detected, saving to `{}`",
-                        patch_path.user_display()
-                    )
-                    .yellow()
-                );
-                fs_err::create_dir_all(patch_dir)?;
-                fs_err::write(&patch_path, output.stdout)?;
-
-                // Clean the working tree
-                Self::checkout_working_tree()?;
-
-                Ok(Self(Some(patch_path)))
-            }
-        } else {
-            Err(cmd.check_status(output.status).unwrap_err().into())
-        }
-    }
-
-    fn checkout_working_tree() -> Result<()> {
-        let status = Command::new(GIT.as_ref()?)
-            .arg("-c")
-            .arg("submodule.recurse=0")
-            .arg("checkout")
-            .arg("--")
-            .arg(".")
-            // prevent recursive post-checkout hooks
-            .env("_PRE_COMMIT_SKIP_POST_CHECKOUT", "1")
-            .stdout(std::process::Stdio::null())
-            .stderr(std::process::Stdio::null())
-            .status()?;
-        if status.success() {
-            Ok(())
-        } else {
-            Err(anyhow::anyhow!("Failed to checkout working tree"))
-        }
-    }
-
-    fn git_apply(patch: &Path) -> Result<()> {
-        let status = Command::new(GIT.as_ref()?)
-            .arg("apply")
-            .arg("--whitespace=nowarn")
-            .arg(patch)
-            .stdout(std::process::Stdio::null())
-            .stderr(std::process::Stdio::null())
-            .status()?;
-        if status.success() {
-            Ok(())
-        } else {
-            Err(anyhow::anyhow!("Failed to apply the patch"))
-        }
-    }
-
-    fn restore(&self) -> Result<()> {
-        let Some(patch) = self.0.as_ref() else {
-            return Ok(());
-        };
-
-        // Try to apply the patch
-        if Self::git_apply(patch).is_err() {
-            error!("Failed to apply the patch, rolling back changes");
-            eprintln!(
-                "{}",
-                "Failed to apply the patch, rolling back changes".red()
-            );
-
-            Self::checkout_working_tree()?;
-            Self::git_apply(patch)?;
-        };
-
-        eprintln!(
-            "{}",
-            format!(
-                "\nRestored working tree changes from `{}`",
-                patch.user_display()
-            )
-            .yellow()
-        );
-
-        Ok(())
-    }
-}
-
-impl Drop for WorkingTreeKeeper {
-    fn drop(&mut self) {
-        if let Err(err) = self.restore() {
-            eprintln!(
-                "{}",
-                format!("Failed to restore working tree changes: {err}").red()
-            );
-        }
-    }
-}
-
-/// Clean Git intent-to-add files and working tree changes, and restore them when dropped.
-pub struct WorkTreeKeeper {
-    intent_to_add: Option<IntentToAddKeeper>,
-    working_tree: Option<WorkingTreeKeeper>,
-}
-
-#[derive(Default)]
-pub struct RestoreGuard {
-    _guard: (),
-}
-
-impl Drop for RestoreGuard {
-    fn drop(&mut self) {
-        if let Some(mut keeper) = RESTORE_WORKTREE.lock().unwrap().take() {
-            keeper.restore();
-        }
-    }
-}
-
-impl WorkTreeKeeper {
-    /// Clear intent-to-add changes from the index and clear the non-staged changes from the working directory.
-    /// Restore them when the instance is dropped.
-    pub async fn clean(store: &Store) -> Result<RestoreGuard> {
-        let cleaner = Self {
-            intent_to_add: Some(IntentToAddKeeper::clean().await?),
-            working_tree: Some(WorkingTreeKeeper::clean(store.path()).await?),
-        };
-
-        // Set to the global for the cleanup hook.
-        *RESTORE_WORKTREE.lock().unwrap() = Some(cleaner);
-
-        // Make sure restoration when ctrl-c is pressed.
-        add_cleanup(|| {
-            if let Some(guard) = &mut *RESTORE_WORKTREE.lock().unwrap() {
-                guard.restore();
-            }
-        });
-
-        Ok(RestoreGuard::default())
-    }
-
-    /// Restore the intent-to-add changes and non-staged changes.
-    fn restore(&mut self) {
-        self.intent_to_add.take();
-        self.working_tree.take();
-    }
-}


Test patch:
diff --git a/src/snapshots/prefligit__config__tests__read_config.snap b/src/snapshots/prefligit__config__tests__read_config.snap
index 6d141bf..d530520 100644
--- a/src/snapshots/prefligit__config__tests__read_config.snap
+++ b/src/snapshots/prefligit__config__tests__read_config.snap
@@ -102,7 +102,6 @@ ConfigWire {
         ),
         Local(
             ConfigLocalRepo {
-                repo: "local",
                 hooks: [
                     ManifestHook {
                         id: "cargo-fmt",
@@ -141,7 +140,6 @@ ConfigWire {
         ),
         Local(
             ConfigLocalRepo {
-                repo: "local",
                 hooks: [
                     ManifestHook {
                         id: "cargo-dev-generate-all",
diff --git a/tests/run.rs b/tests/run.rs
index 7640102..fa273cf 100644
--- a/tests/run.rs
+++ b/tests/run.rs
@@ -183,6 +183,68 @@ fn local_need_install() {
     "#);
 }
 
+#[test]
+fn meta_hooks() -> Result<()> {
+    let context = TestContext::new();
+    context.init_project();
+
+    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: false
+    exit_code: 1
+    ----- stdout -----
+    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]
 fn invalid_hook_id() {
     let context = TestContext::new();