use colored::Colorize;
use std::process::ExitCode;
use super::metadata::save_installed_hook;
use super::script::{build_hook_command, build_thin_wrapper_script, hook_action};
use super::{find_git_root, is_command_available, write_hook_script};
use crate::cli::commands::{HookEvent, HookTool};
pub(crate) fn format_hook_source(source: &linthis::config::HookSource) -> String {
use linthis::config::HookSource;
match source {
HookSource::Plugin { plugin, file } => {
format!("{{ plugin = \"{}\", file = \"{}\" }}", plugin, file)
}
HookSource::File { file } => {
format!("{{ file = \"{}\" }}", file)
}
HookSource::Url { url } => {
format!("{{ url = \"{}\" }}", url)
}
HookSource::Git { git, git_ref, path } => {
if let Some(r) = git_ref {
format!(
"{{ git = \"{}\", ref = \"{}\", path = \"{}\" }}",
git, r, path
)
} else {
format!("{{ git = \"{}\", path = \"{}\" }}", git, path)
}
}
HookSource::Marketplace {
marketplace,
plugin,
file,
} => {
format!(
"{{ marketplace = \"{}\", plugin = \"{}\", file = \"{}\" }}",
marketplace, plugin, file
)
}
}
}
pub(crate) fn describe_hook_source(tool: &HookTool, hook_event: &HookEvent) -> String {
use linthis::config::Config;
use linthis::hooks::resolver;
let dir = match tool_type_dir(tool) {
Some(d) => d,
None => return "built-in (agent)".to_string(),
};
let project_root = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
if resolver::fixed_git_hook_path(&project_root, dir, hook_event.hook_filename()).is_some() {
return format!("hooks/{}/{} (fixed path)", dir, hook_event.hook_filename());
}
let config = Config::load_merged(&project_root);
let event_key = hook_event.hook_filename();
if let Some(entry) = lookup_hook_config_entry(&config.hook, tool, event_key) {
let section = format!("[hook.{}]", dir);
let source_str = format_hook_source(&entry.source);
return format!("{}\n{} = {{ source = {} }}", section, event_key, source_str);
}
let cmd = build_hook_command(hook_event, &None);
format!("built-in → {}", cmd)
}
pub(crate) fn tool_type_dir(tool: &HookTool) -> Option<&'static str> {
match tool {
HookTool::Git => Some("git"),
HookTool::GitWithAgent => Some("git-with-agent"),
HookTool::Prek => Some("prek"),
HookTool::PrekWithAgent => Some("prek-with-agent"),
HookTool::Agent => None,
}
}
pub(crate) fn lookup_hook_config_entry<'a>(
hook_cfg: &'a linthis::config::HookConfig,
tool: &HookTool,
event_key: &str,
) -> Option<&'a linthis::config::HookSourceEntry> {
match tool {
HookTool::Git => hook_cfg.git.get(event_key),
HookTool::GitWithAgent => hook_cfg.git_with_agent.get(event_key),
HookTool::Prek => hook_cfg.prek.get(event_key),
HookTool::PrekWithAgent => hook_cfg.prek_with_agent.get(event_key),
HookTool::Agent => None,
}
}
pub(crate) fn resolve_hook_override(
tool: &HookTool,
hook_event: &HookEvent,
) -> Result<Option<String>, ExitCode> {
use linthis::config::Config;
use linthis::hooks::resolver;
let dir = match tool_type_dir(tool) {
Some(d) => d,
None => return Ok(None),
};
let project_root = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
if let Some(fixed) =
resolver::fixed_git_hook_path(&project_root, dir, hook_event.hook_filename())
{
match std::fs::read_to_string(fixed.as_path()) {
Ok(content) => return Ok(Some(content)),
Err(e) => {
eprintln!(
"{}: Failed to read fixed-path override '{}': {}",
"Error".red(),
fixed.display(),
e
);
return Err(ExitCode::from(2));
}
}
}
let config = Config::load_merged(&project_root);
let event_key = hook_event.hook_filename();
if let Some(entry) = lookup_hook_config_entry(&config.hook, tool, event_key) {
match resolver::resolve_to_string(&entry.source, &project_root, &config.hook.marketplaces) {
Ok(content) => return Ok(Some(content)),
Err(e) => {
eprintln!(
"{}: Failed to resolve hook override for '{}/{}': {}",
"Error".red(),
dir,
event_key,
e
);
return Err(ExitCode::from(2));
}
}
}
Ok(None)
}
fn create_prek_config(
tool: &HookTool,
hook_event: &HookEvent,
force: bool,
args: &Option<String>,
) -> Result<(), ExitCode> {
let config_path = std::path::PathBuf::from(".pre-commit-config.yaml");
let hook_filename = hook_event.hook_filename();
if config_path.exists() && !force {
eprintln!(
"{}: {} already exists, skipping",
"Warning".yellow(),
config_path.display()
);
return Ok(());
}
if let Some(override_content) = resolve_hook_override(tool, hook_event)? {
std::fs::write(&config_path, override_content).map_err(|e| {
eprintln!(
"{}: Failed to write '{}': {}",
"Error".red(),
config_path.display(),
e
);
ExitCode::from(2)
})?;
println!(
"{} Created {} [override]",
"✓".green(),
config_path.display()
);
return Ok(());
}
let hook_cmd = build_hook_command(hook_event, args);
let stage = hook_event.hook_filename();
let content = format!(
"repos:\n - repo: local\n hooks:\n - id: linthis-{}\n name: linthis ({})\n entry: {}\n language: system\n stages: [{}]\n pass_filenames: false\n",
hook_filename, hook_event.description(), hook_cmd, stage
);
std::fs::write(&config_path, content).map_err(|e| {
eprintln!(
"{}: Failed to create {}: {}",
"Error".red(),
config_path.display(),
e
);
ExitCode::from(2)
})?;
let tool_name = "prek";
println!(
"{} Created {} ({}/pre-commit compatible)",
"✓".green(),
config_path.display(),
tool_name
);
print_prek_next_steps(tool, hook_event, hook_filename);
Ok(())
}
fn print_prek_next_steps(tool: &HookTool, hook_event: &HookEvent, hook_filename: &str) {
let tool_name = "prek";
if is_command_available(tool_name) {
println!("\n{} Detected installed", tool_name.cyan());
print!("{} Installing hooks... ", "→".cyan());
std::io::Write::flush(&mut std::io::stdout()).ok();
match install_hooks(tool, hook_event) {
Ok(_) => {
println!("{}", "✓".green());
println!(
"\n{} {} hooks are ready!",
"✓".green().bold(),
hook_filename
);
println!(
" Hooks will run automatically on {}",
format!("git {}", hook_action(hook_event)).cyan()
);
}
Err(e) => {
println!("{}", "✗".red());
eprintln!("{}: {}", "Warning".yellow(), e);
println!(
"\nPlease run manually: {}",
format!("{} install --hook-type {}", tool_name, hook_filename).cyan()
);
}
}
} else {
println!("\nNext steps:");
if matches!(tool, HookTool::Prek) {
let prek_cmd = linthis::python_tool_install_hint("prek").replace("Install: ", "");
println!(" 1. Install prek: {}", prek_cmd.cyan());
println!(
" 2. Set up hooks: {}",
format!("prek install --hook-type {}", hook_filename).cyan()
);
} else {
let precommit_cmd = linthis::python_tool_install_hint("pre-commit").replace("Install: ", "");
println!(
" 1. Install pre-commit: {}",
precommit_cmd.cyan()
);
println!(
" 2. Set up hooks: {}",
format!("pre-commit install --hook-type {}", hook_filename).cyan()
);
}
}
}
fn install_hooks(tool: &HookTool, hook_event: &HookEvent) -> Result<(), String> {
use std::process::Command;
let (cmd, tool_name) = match tool {
HookTool::Prek => ("prek", "prek"),
HookTool::Git | HookTool::Agent | HookTool::GitWithAgent | HookTool::PrekWithAgent => {
return Ok(()); }
};
let hook_type_arg = hook_event.hook_filename();
let output = Command::new(cmd)
.arg("install")
.arg("--hook-type")
.arg(hook_type_arg)
.output()
.map_err(|e| format!("Failed to execute {} install: {}", tool_name, e))?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(format!("{} install failed: {}", tool_name, stderr))
}
}
fn create_git_hook_config(
tool: &HookTool,
hook_event: &HookEvent,
force: bool,
args: &Option<String>,
) -> Result<(), ExitCode> {
use std::fs;
let hook_filename = hook_event.hook_filename();
let git_root = match find_git_root() {
Some(root) => root,
None => {
eprintln!(
"{}: Not in a git repository, cannot create .git/hooks/{}",
"Error".red(),
hook_filename
);
return Err(ExitCode::from(1));
}
};
let git_hooks_dir = git_root.join(".git/hooks");
let hook_path = git_hooks_dir.join(hook_filename);
if !git_hooks_dir.exists() {
fs::create_dir_all(&git_hooks_dir).map_err(|e| {
eprintln!(
"{}: Failed to create hooks directory {}: {}",
"Error".red(),
git_hooks_dir.display(),
e
);
ExitCode::from(2)
})?;
}
if let Some(override_content) = resolve_hook_override(tool, hook_event)? {
return write_git_hook_override(&hook_path, &override_content, force);
}
let linthis_hook_line = build_hook_command(hook_event, args);
if hook_path.exists() {
return append_linthis_to_existing_hook(&hook_path, &linthis_hook_line);
}
let content = build_thin_wrapper_script(hook_event, &HookTool::Git, None, false, None);
write_hook_script(&hook_path, &content)?;
println!("{} Created {} [project]", "✓".green(), hook_path.display());
println!(
" {} Thin wrapper: hook logic auto-updates with linthis",
"→".dimmed()
);
#[cfg(not(unix))]
{
println!("\nNext steps:");
println!(" Make sure the hook is executable:");
println!(
" {}",
format!("chmod +x .git/hooks/{}", hook_filename).cyan()
);
}
let project = git_root.to_str().unwrap_or("").to_string();
save_installed_hook("local", &project, hook_event, &HookTool::Git, None, None);
Ok(())
}
fn write_git_hook_override(
hook_path: &std::path::Path,
override_content: &str,
force: bool,
) -> Result<(), ExitCode> {
let content = if hook_path.exists() && !force {
let mut existing = std::fs::read_to_string(hook_path).unwrap_or_default();
if !existing.ends_with('\n') {
existing.push('\n');
}
existing.push_str("\n# linthis-hook (override)\n");
existing.push_str(override_content);
existing
} else {
override_content.to_string()
};
write_hook_script(hook_path, &content)?;
println!(
"{} Created {} [project, override]",
"✓".green(),
hook_path.display()
);
Ok(())
}
fn append_linthis_to_existing_hook(
hook_path: &std::path::Path,
linthis_hook_line: &str,
) -> Result<(), ExitCode> {
let existing_content = std::fs::read_to_string(hook_path).map_err(|e| {
eprintln!(
"{}: Failed to read existing hook file: {}",
"Error".red(),
e
);
ExitCode::from(2)
})?;
if existing_content.contains(linthis_hook_line) || existing_content.contains("linthis hook run")
{
println!(
"{}: linthis hook already exists in {}",
"Info".cyan(),
hook_path.display()
);
return Ok(());
}
let mut new_content = existing_content;
if !new_content.ends_with('\n') {
new_content.push('\n');
}
new_content.push_str("\n# linthis-hook\n");
new_content.push_str(linthis_hook_line);
new_content.push('\n');
std::fs::write(hook_path, new_content).map_err(|e| {
eprintln!(
"{}: Failed to update {}: {}",
"Error".red(),
hook_path.display(),
e
);
ExitCode::from(2)
})?;
println!(
"{} Added linthis to existing {} {} [project]",
"✓".green(),
hook_path.display(),
"(appended)".dimmed()
);
Ok(())
}
pub(crate) fn create_hook_config(
tool: &HookTool,
hook_event: &HookEvent,
force: bool,
args: &Option<String>,
) -> Result<(), ExitCode> {
match tool {
HookTool::Agent | HookTool::GitWithAgent | HookTool::PrekWithAgent => Ok(()),
HookTool::Prek => create_prek_config(tool, hook_event, force, args),
HookTool::Git => create_git_hook_config(tool, hook_event, force, args),
}
}