use colored::Colorize;
use std::process::ExitCode;
use super::agent::handle_agent_hook_uninstall;
use super::metadata::remove_installed_hook;
use super::{find_git_root, global_hooks_dir, is_linthis_hook_file};
use crate::cli::commands::{HookEvent, HookTool};
fn handle_global_hook_uninstall(hook_event: Option<HookEvent>, all: bool, yes: bool) -> ExitCode {
use std::fs;
use std::io::{self, Write};
let hooks_dir = match global_hooks_dir() {
Some(d) => d,
None => {
eprintln!("{}: Could not determine home directory", "Error".red());
return ExitCode::from(1);
}
};
let events_to_remove: Vec<HookEvent> = if all {
vec![
HookEvent::PreCommit,
HookEvent::PrePush,
HookEvent::CommitMsg,
]
} else {
vec![hook_event.unwrap_or(HookEvent::PreCommit)]
};
let mut any_removed = false;
for event in &events_to_remove {
let hook_path = hooks_dir.join(event.hook_filename());
if !hook_path.exists() || !is_linthis_hook_file(&hook_path) {
continue;
}
if !yes {
print!(
"Remove global {} hook at {}? [y/N]: ",
event.hook_filename().cyan(),
hook_path.display()
);
io::stdout().flush().ok();
let mut input = String::new();
io::stdin().read_line(&mut input).ok();
if !matches!(input.trim().to_lowercase().as_str(), "y" | "yes") {
println!("Skipped {}", event.hook_filename());
continue;
}
}
match fs::remove_file(&hook_path) {
Ok(_) => {
println!(
"{} Removed global {} hook",
"✓".green(),
event.hook_filename()
);
remove_installed_hook("global", "", event);
any_removed = true;
}
Err(e) => {
eprintln!(
"{}: Failed to remove {}: {}",
"Error".red(),
hook_path.display(),
e
);
}
}
}
if !any_removed {
println!("{}: No global linthis hooks found", "Info".cyan());
return ExitCode::SUCCESS;
}
let remaining = [
HookEvent::PreCommit,
HookEvent::PrePush,
HookEvent::CommitMsg,
]
.iter()
.any(|e| is_linthis_hook_file(&hooks_dir.join(e.hook_filename())));
if !remaining {
let _ = std::process::Command::new("git")
.args(["config", "--global", "--unset", "core.hooksPath"])
.status();
println!("{} Unset global {}", "✓".green(), "core.hooksPath".cyan());
}
ExitCode::SUCCESS
}
fn remove_linthis_lines_from_hook(
hook_path: &std::path::Path,
existing_content: &str,
) -> Result<(), ExitCode> {
let new_content: String = existing_content
.lines()
.filter(|line| !line.contains("linthis") && !line.contains("# linthis-hook"))
.collect::<Vec<_>>()
.join("\n");
if let Err(e) = std::fs::write(hook_path, new_content + "\n") {
eprintln!("{}: Failed to update hook: {}", "Error".red(), e);
return Err(ExitCode::from(2));
}
Ok(())
}
fn delete_hook_file(hook_path: &std::path::Path) -> Result<(), ExitCode> {
if let Err(e) = std::fs::remove_file(hook_path) {
eprintln!("{}: Failed to delete hook: {}", "Error".red(), e);
return Err(ExitCode::from(2));
}
Ok(())
}
fn hook_has_other_content(content: &str) -> bool {
content.lines().any(|line| {
let trimmed = line.trim();
!trimmed.is_empty()
&& !trimmed.starts_with('#')
&& !trimmed.starts_with("#!/")
&& !trimmed.contains("linthis")
})
}
pub(crate) fn uninstall_single_hook(
git_root: &std::path::Path,
hook_event: &HookEvent,
yes: bool,
) -> ExitCode {
let hook_path = git_root.join(".git/hooks").join(hook_event.hook_filename());
if !hook_path.exists() {
return ExitCode::from(1);
}
let existing_content = match std::fs::read_to_string(&hook_path) {
Ok(content) => content,
Err(e) => {
eprintln!("{}: Failed to read hook file: {}", "Error".red(), e);
return ExitCode::from(2);
}
};
if !existing_content.contains("linthis") {
return ExitCode::from(1);
}
let has_other = hook_has_other_content(&existing_content);
let result = if yes {
uninstall_hook_auto(&hook_path, &existing_content, has_other, hook_event)
} else {
uninstall_hook_interactive(&hook_path, &existing_content, has_other)
};
if let Err(code) = result {
return code;
}
let project_str = git_root.to_str().unwrap_or("").to_string();
remove_installed_hook("local", &project_str, hook_event);
ExitCode::SUCCESS
}
fn uninstall_hook_auto(
hook_path: &std::path::Path,
existing_content: &str,
has_other: bool,
hook_event: &HookEvent,
) -> Result<(), ExitCode> {
if has_other {
remove_linthis_lines_from_hook(hook_path, existing_content)?;
println!(
"{} Removed linthis from {} hook",
"✓".green(),
hook_event.hook_filename()
);
} else {
delete_hook_file(hook_path)?;
println!(
"{} Deleted {} hook",
"✓".green(),
hook_event.hook_filename()
);
}
Ok(())
}
fn uninstall_hook_interactive(
hook_path: &std::path::Path,
existing_content: &str,
has_other: bool,
) -> Result<(), ExitCode> {
use std::io::{self, Write};
println!("{}: {} contains:", "Warning".yellow(), hook_path.display());
println!(" {} linthis", "✓".green());
if has_other {
println!(" {} Other hooks/commands", "⚠".yellow());
}
println!("\nOptions:");
if has_other {
println!(
" 1. {} - Remove only linthis lines",
"Remove linthis".cyan()
);
println!(" 2. {} - Delete entire hook file", "Delete all".cyan());
} else {
println!(" 1. {} - Delete hook file", "Delete".cyan());
}
println!(" 3. {} - Cancel", "Cancel".cyan());
print!("\nChoose an option: ");
io::stdout().flush().unwrap();
let mut choice = String::new();
io::stdin().read_line(&mut choice).ok();
match choice.trim() {
"1" => {
if has_other {
remove_linthis_lines_from_hook(hook_path, existing_content)?;
println!(
"{} Removed linthis from {}",
"✓".green(),
hook_path.display()
);
} else {
delete_hook_file(hook_path)?;
println!("{} Deleted {}", "✓".green(), hook_path.display());
}
}
"2" if has_other => {
delete_hook_file(hook_path)?;
println!("{} Deleted {}", "✓".green(), hook_path.display());
}
_ => {
println!("Uninstall cancelled");
}
}
Ok(())
}
fn uninstall_global_hooks(
effective_events: &[HookEvent],
include_git: bool,
include_agent: bool,
all_events: bool,
yes: bool,
) -> ExitCode {
let mut result = ExitCode::SUCCESS;
if include_git {
if all_events {
result = handle_global_hook_uninstall(None, true, yes);
} else {
for event in effective_events {
result = handle_global_hook_uninstall(Some(event.clone()), false, yes);
}
}
}
if include_agent {
handle_agent_hook_uninstall(yes, true, effective_events);
}
result
}
fn uninstall_local_hooks(
effective_events: &[HookEvent],
include_git: bool,
include_agent: bool,
yes: bool,
) -> ExitCode {
let git_root = match find_git_root() {
Some(root) => root,
None => {
eprintln!("{}: Not in a git repository", "Error".red());
return ExitCode::from(1);
}
};
let mut any_uninstalled = false;
if include_git {
for event in effective_events {
if uninstall_single_hook(&git_root, event, yes) == ExitCode::SUCCESS {
any_uninstalled = true;
}
}
}
if include_agent
&& handle_agent_hook_uninstall(yes, false, effective_events) == ExitCode::SUCCESS
{
any_uninstalled = true;
}
if !any_uninstalled {
println!("{}: No hooks with linthis found", "Info".cyan());
}
ExitCode::SUCCESS
}
pub(crate) fn handle_hook_uninstall(
hook_types: Vec<HookTool>,
hook_events: Vec<HookEvent>,
all: bool,
all_types: bool,
all_events: bool,
yes: bool,
global: bool,
) -> ExitCode {
const ALL_EVENTS_LIST: [HookEvent; 3] = [
HookEvent::PreCommit,
HookEvent::PrePush,
HookEvent::CommitMsg,
];
let effective_events: Vec<HookEvent> = if all || all_events {
ALL_EVENTS_LIST.to_vec()
} else {
hook_events
};
let include_agent = all || all_types || hook_types.iter().any(|t| matches!(t, HookTool::Agent));
let include_git = all || all_types || hook_types.iter().any(|t| !matches!(t, HookTool::Agent));
if global {
uninstall_global_hooks(
&effective_events,
include_git,
include_agent,
all || all_events,
yes,
)
} else {
uninstall_local_hooks(&effective_events, include_git, include_agent, yes)
}
}