use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PreCommitSystem {
Prek,
PreCommit,
Husky,
Lefthook,
None,
}
impl PreCommitSystem {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Prek => "prek",
Self::PreCommit => "pre-commit",
Self::Husky => "husky",
Self::Lefthook => "lefthook",
Self::None => "none",
}
}
}
impl std::fmt::Display for PreCommitSystem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[must_use]
pub fn detect_pre_commit_system(project_root: &Path) -> PreCommitSystem {
detect_pre_commit_system_with(project_root, prek_on_path())
}
#[must_use]
pub(crate) fn detect_pre_commit_system_with(
project_root: &Path,
prek_on_path: bool,
) -> PreCommitSystem {
let pre_commit_yaml = project_root.join(".pre-commit-config.yaml");
let pre_commit_yml = project_root.join(".pre-commit-config.yml");
let pre_commit_path = if pre_commit_yaml.is_file() {
Some(pre_commit_yaml)
} else if pre_commit_yml.is_file() {
Some(pre_commit_yml)
} else {
None
};
if let Some(pre_commit_path) = pre_commit_path {
if has_prek_marker(project_root, &pre_commit_path, prek_on_path) {
return PreCommitSystem::Prek;
}
return PreCommitSystem::PreCommit;
}
if has_husky(project_root) {
return PreCommitSystem::Husky;
}
if has_lefthook(project_root) {
return PreCommitSystem::Lefthook;
}
PreCommitSystem::None
}
fn has_prek_marker(project_root: &Path, pre_commit_path: &Path, prek_on_path: bool) -> bool {
if prek_on_path {
return true;
}
if mise_mentions_prek(project_root) {
return true;
}
pre_commit_yaml_mentions_prek(pre_commit_path)
}
fn prek_on_path() -> bool {
let Some(path) = std::env::var_os("PATH") else {
return false;
};
for dir in std::env::split_paths(&path) {
if dir.join("prek").is_file() {
return true;
}
if dir.join("prek.exe").is_file() {
return true;
}
}
false
}
fn mise_mentions_prek(project_root: &Path) -> bool {
let candidates = [
project_root.join("mise.toml"),
project_root.join(".mise.toml"),
];
candidates.iter().any(|p| file_contains(p, "prek"))
}
fn pre_commit_yaml_mentions_prek(path: &Path) -> bool {
file_contains(path, "prek")
}
fn file_contains(path: &Path, needle: &str) -> bool {
let Ok(content) = std::fs::read_to_string(path) else {
return false;
};
content.contains(needle)
}
fn has_husky(project_root: &Path) -> bool {
if project_root.join(".husky").is_dir() {
return true;
}
package_json_has_husky_key(&project_root.join("package.json"))
}
fn package_json_has_husky_key(path: &Path) -> bool {
let Ok(content) = std::fs::read_to_string(path) else {
return false;
};
content.contains("\"husky\"")
}
fn has_lefthook(project_root: &Path) -> bool {
const LEFTHOOK_FILES: &[&str] = &[
"lefthook.yml",
"lefthook.yaml",
".lefthook.yml",
".lefthook.yaml",
];
LEFTHOOK_FILES
.iter()
.map(|name| project_root.join(name))
.any(|p: PathBuf| p.is_file())
}
#[cfg(test)]
#[path = "pre_commit_detect_tests.rs"]
mod pre_commit_detect_tests;