use std::collections::HashSet;
use anyhow::Result;
use crate::{cli::banner, cli::output::*, config::CONFIG};
pub async fn validate_hooks_config() -> Result<()> {
banner::print_banner(None);
styled!("Validating {} configuration...", ("hooks", "primary"));
let hooks_config = &CONFIG.hooks;
let mut errors = Vec::new();
let mut warnings = Vec::new();
validate_hook_definition(
"pre-commit",
&hooks_config.pre_commit,
&mut errors,
&mut warnings,
)
.await;
validate_hook_definition(
"commit-msg",
&hooks_config.commit_msg,
&mut errors,
&mut warnings,
)
.await;
validate_hook_definition(
"post-checkout",
&hooks_config.post_checkout,
&mut errors,
&mut warnings,
)
.await;
validate_hook_definition(
"post-commit",
&hooks_config.post_commit,
&mut errors,
&mut warnings,
)
.await;
validate_hook_definition(
"post-merge",
&hooks_config.post_merge,
&mut errors,
&mut warnings,
)
.await;
validate_global_config(hooks_config, &mut warnings).await;
if errors.is_empty() && warnings.is_empty() {
styled!("{} Configuration is valid!", ("✅", "success_symbol"));
return Ok(());
}
if !warnings.is_empty() {
styled!("{} Warnings:", ("⚠️", "warning_symbol"));
for warning in &warnings {
styled!(" • {}", (warning, "warning"));
}
}
if !errors.is_empty() {
styled!("{} Errors:", ("❌", "error_symbol"));
for error in &errors {
styled!(" • {}", (error, "error"));
}
styled!(
"{} Configuration validation failed!",
("❌", "error_symbol")
);
return Err(anyhow::anyhow!(
"Hook configuration has {} errors",
errors.len()
));
}
if !warnings.is_empty() {
styled!(
"{} Configuration is valid with warnings",
("⚠️", "warning_symbol")
);
}
Ok(())
}
async fn validate_hook_definition(
hook_name: &str,
hook_def: &crate::config::hooks::HookDefinition,
errors: &mut Vec<String>,
warnings: &mut Vec<String>,
) {
if hook_def.commands.is_empty() && hook_def.scripts.is_empty() {
return;
}
styled!("Validating {} hook...", (hook_name, "property"));
let mut command_names = HashSet::new();
for (name, command) in &hook_def.commands {
if !command_names.insert(name) {
errors.push(format!("{hook_name}: Duplicate command name '{name}'"));
}
if command.run.is_empty() {
errors.push(format!(
"{hook_name}: Command '{name}' has empty 'run' field"
));
}
if command.run.contains("guardy") && command.run.contains("hooks run") {
warnings.push(format!(
"{hook_name}: Command '{name}' appears to call guardy hooks recursively"
));
}
for glob_pattern in &*command.glob {
if glob_pattern.is_empty() {
warnings.push(format!(
"{hook_name}: Command '{name}' has empty glob pattern"
));
}
if globset::Glob::new(glob_pattern).is_err() {
errors.push(format!(
"{hook_name}: Command '{name}' has invalid glob pattern '{glob_pattern}'"
));
}
}
for file_type in &command.file_types {
if file_type.is_empty() {
warnings.push(format!("{hook_name}: Command '{name}' has empty file type"));
}
}
for (env_key, env_value) in &command.env {
if env_key.is_empty() {
warnings.push(format!(
"{hook_name}: Command '{name}' has empty environment variable name"
));
}
if env_value.is_empty() {
warnings.push(format!(
"{hook_name}: Command '{name}' has empty environment variable value for \
'{env_key}'"
));
}
}
if command.description.is_empty() {
warnings.push(format!("{hook_name}: Command '{name}' has no description"));
}
}
let mut script_names = HashSet::new();
for (name, script) in &hook_def.scripts {
if !script_names.insert(name) {
errors.push(format!("{hook_name}: Duplicate script name '{name}'"));
}
if command_names.contains(name) {
errors.push(format!(
"{hook_name}: Name '{name}' is used for both command and script"
));
}
if script.runner.is_empty() {
errors.push(format!(
"{hook_name}: Script '{name}' has empty 'runner' field"
));
}
let common_runners = ["bash", "sh", "node", "python", "python3", "ruby", "perl"];
if !common_runners.contains(&script.runner.as_str()) && !script.runner.starts_with('/') {
warnings.push(format!(
"{hook_name}: Script '{name}' uses uncommon runner '{}'",
script.runner
));
}
}
match hook_name {
"pre-commit" => {
if hook_def.commands.is_empty() && hook_def.scripts.is_empty() {
warnings
.push("pre-commit: Hook is enabled but has no commands or scripts".to_string());
}
}
"commit-msg" => {
let has_msg_validation = hook_def.commands.values().any(|cmd| {
cmd.run.contains("conventional")
|| cmd.run.contains("commitizen")
|| cmd.run.contains("validate")
});
if !has_msg_validation {
warnings.push("commit-msg: Consider adding commit message validation".to_string());
}
}
_ => {}
}
}
async fn validate_global_config(
hooks_config: &crate::config::hooks::HooksConfig,
warnings: &mut Vec<String>,
) {
if hooks_config.skip_all {
warnings
.push("Global setting 'skip_all' is enabled - all hooks will be skipped".to_string());
}
let has_any_hooks = !hooks_config.pre_commit.commands.is_empty()
|| !hooks_config.pre_commit.scripts.is_empty()
|| !hooks_config.commit_msg.commands.is_empty()
|| !hooks_config.commit_msg.scripts.is_empty()
|| !hooks_config.post_checkout.commands.is_empty()
|| !hooks_config.post_checkout.scripts.is_empty()
|| !hooks_config.post_commit.commands.is_empty()
|| !hooks_config.post_commit.scripts.is_empty()
|| !hooks_config.post_merge.commands.is_empty()
|| !hooks_config.post_merge.scripts.is_empty();
if !has_any_hooks {
warnings
.push("No hooks are configured - consider adding some commands or scripts".to_string());
}
}