use colored::Colorize;
use super::config::describe_hook_source;
use super::script::{
build_git_with_agent_hook_script, build_global_hook_script_for_event, build_hook_command,
build_post_commit_script, build_post_commit_with_agent_script, merge_model_into_provider_args,
parse_agent_fix_provider_name, parse_provider_with_model,
};
use crate::cli::commands::{AgentFixProvider, HookEvent, HookTool};
const LINTHIS_HOOK_RUNNING_PREFIX: &str = "LINTHIS_HOOK_RUNNING_";
pub(crate) fn build_reentrant_git_script(event: &HookEvent) -> String {
let linthis_cmd = build_hook_command(event, &None);
if matches!(event, HookEvent::PrePush) {
format!(
"#!/bin/sh\n\
_BASE=$(git rev-parse '@{{u}}' 2>/dev/null || \\\n\
\x20 git rev-parse 'HEAD~1' 2>/dev/null)\n\
_PUSHED_FILES=$(git diff --name-only \"$_BASE\"..HEAD 2>/dev/null | grep -v '^$')\n\
if [ -n \"$_PUSHED_FILES\" ]; then\n\
\x20 set --\n\
\x20 while IFS= read -r _F; do set -- \"$@\" -i \"$_F\"; done <<_EOF_\n\
$_PUSHED_FILES\n\
_EOF_\n\
\x20 {linthis} \"$@\"\n\
fi\n",
linthis = linthis_cmd
)
} else {
format!("#!/bin/sh\n{linthis_cmd} \"$@\"\n")
}
}
pub(crate) fn handle_hook_run(
event: &HookEvent,
hook_type: &HookTool,
raw_provider: Option<&str>,
raw_provider_args: Option<&str>,
_global: bool,
hook_args: &[String],
) -> i32 {
if super::skip::should_skip(event) {
return 0;
}
let (provider_name, merged_pa) = if let Some(raw) = raw_provider {
let (name, model) = parse_provider_with_model(raw);
(
Some(name),
merge_model_into_provider_args(model, raw_provider_args),
)
} else {
(None, raw_provider_args.map(|s| s.to_string()))
};
let provider: Option<&str> = provider_name;
let provider_args: Option<&str> = merged_pa.as_deref();
let already_running = std::env::vars().any(|(k, _)| k.starts_with(LINTHIS_HOOK_RUNNING_PREFIX));
let script = if matches!(event, HookEvent::PostCommit) {
if already_running {
return 0;
}
let linthis_fmt_cmd = build_hook_command(event, &None);
if matches!(hook_type, HookTool::GitWithAgent) {
let fix_provider = provider
.and_then(parse_agent_fix_provider_name)
.unwrap_or(AgentFixProvider::Claude);
build_post_commit_with_agent_script(&linthis_fmt_cmd, &fix_provider, provider_args)
} else {
build_post_commit_script(&linthis_fmt_cmd)
}
} else {
match hook_type {
HookTool::Git => {
if already_running {
build_reentrant_git_script(event)
} else {
build_global_hook_script_for_event(event, &None, None)
}
}
HookTool::GitWithAgent => {
let fix_provider = provider
.and_then(parse_agent_fix_provider_name)
.unwrap_or(AgentFixProvider::Claude);
let linthis_cmd = build_hook_command(event, &None);
build_git_with_agent_hook_script(
&linthis_cmd,
&fix_provider,
event,
provider_args,
)
}
_ => {
eprintln!(
"{}: hook run: unsupported hook type '{}' (supported: git, git-with-agent)",
"Error".red(),
hook_type.as_str()
);
return 1;
}
}
};
if is_rebase_in_progress() {
return 0;
}
let captured_stdin: Option<Vec<u8>> = if matches!(event, HookEvent::PrePush) {
let (is_empty, buf) = read_pre_push_stdin();
if is_empty {
return 0;
}
Some(buf)
} else {
None
};
{
let description = describe_hook_source(hook_type, event);
eprintln!("{}", format!("📄 Config: {}", description).dimmed());
}
let pid = std::process::id().to_string();
let env_key = format!("{}{}", LINTHIS_HOOK_RUNNING_PREFIX, pid);
let status = run_hook_script(&script, hook_args, &env_key, captured_stdin);
match status {
Ok(s) => s.code().unwrap_or(1),
Err(e) => {
eprintln!(
"{}: hook run: failed to execute script: {}",
"Error".red(),
e
);
1
}
}
}
fn run_hook_script(
script: &str,
hook_args: &[String],
env_key: &str,
captured_stdin: Option<Vec<u8>>,
) -> std::io::Result<std::process::ExitStatus> {
let mut sh_cmd = std::process::Command::new("sh");
sh_cmd
.arg("-c")
.arg(script)
.arg("--")
.args(hook_args)
.env(env_key, "1");
if let Some(stdin_buf) = captured_stdin {
use std::io::Write;
let mut child = sh_cmd.stdin(std::process::Stdio::piped()).spawn()?;
if let Some(mut child_stdin) = child.stdin.take() {
let _ = child_stdin.write_all(&stdin_buf);
}
child.wait()
} else {
sh_cmd.status()
}
}
fn read_pre_push_stdin() -> (bool, Vec<u8>) {
use std::io::Read;
let mut buf = Vec::new();
if std::io::stdin().read_to_end(&mut buf).is_err() {
return (false, buf);
}
let content = String::from_utf8_lossy(&buf);
let mut has_push = false;
for line in content.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 4 {
if parts[1] != parts[3] {
has_push = true;
break;
}
} else if !parts.is_empty() {
has_push = true;
break;
}
}
(!has_push, buf)
}
fn is_rebase_in_progress() -> bool {
if let Ok(output) = std::process::Command::new("git")
.args(["rev-parse", "--git-dir"])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.output()
{
if output.status.success() {
let git_dir = String::from_utf8_lossy(&output.stdout).trim().to_string();
let git_path = std::path::Path::new(&git_dir);
return git_path.join("rebase-merge").exists()
|| git_path.join("rebase-apply").exists()
|| git_path.join("MERGE_HEAD").exists()
|| git_path.join("CHERRY_PICK_HEAD").exists();
}
}
false
}