use std::io::{self, IsTerminal, Write};
use std::path::Path;
use anyhow::Context;
use color_print::cformat;
use worktrunk::config::Approvals;
use worktrunk::git::{GitError, HookType};
use worktrunk::styling::{
INFO_SYMBOL, WARNING_SYMBOL, eprint, eprintln, format_bash_with_gutter, hint_message,
prompt_message, stderr, warning_message,
};
use super::hook_filter::{HookSource, ParsedFilter};
use super::project_config::{ApprovableCommand, Phase, collect_commands_for_hooks};
pub fn approve_command_batch(
commands: &[ApprovableCommand],
project_id: &str,
approvals: &Approvals,
yes: bool,
commands_already_filtered: bool,
) -> anyhow::Result<bool> {
let needs_approval: Vec<&ApprovableCommand> = commands
.iter()
.filter(|cmd| {
commands_already_filtered
|| !approvals.is_command_approved(project_id, &cmd.command.template)
})
.collect();
if needs_approval.is_empty() {
return Ok(true);
}
let approved = if yes {
true
} else {
prompt_for_batch_approval(&needs_approval, project_id)?
};
if !approved {
return Ok(false);
}
if !yes {
let mut fresh_approvals = Approvals::load().context("Failed to load approvals")?;
let commands: Vec<String> = needs_approval
.iter()
.map(|cmd| cmd.command.template.clone())
.collect();
if let Err(e) = fresh_approvals.approve_commands(project_id.to_string(), commands, None) {
eprintln!(
"{}",
warning_message(format!("Failed to save command approval: {e}"))
);
eprintln!(
"{}",
hint_message("Approval will be requested again next time.")
);
}
}
Ok(true)
}
fn prompt_for_batch_approval(
commands: &[&ApprovableCommand],
project_id: &str,
) -> anyhow::Result<bool> {
let project_name = Path::new(project_id)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(project_id);
let count = commands.len();
let plural = if count == 1 { "" } else { "s" };
eprintln!(
"{}",
cformat!(
"{WARNING_SYMBOL} <yellow><bold>{project_name}</> needs approval to execute <bold>{count}</> command{plural}:</>"
)
);
for cmd in commands {
let phase = cmd.phase.to_string();
let label = match &cmd.command.name {
Some(name) => cformat!("{INFO_SYMBOL} {phase} <bold>{name}</>:"),
None => format!("{INFO_SYMBOL} {phase}:"),
};
eprintln!("{}", label);
eprintln!("{}", format_bash_with_gutter(&cmd.command.template));
}
if !io::stdin().is_terminal() {
return Err(GitError::NotInteractive.into());
}
worktrunk::styling::eprintln!();
stderr().flush()?;
eprint!(
"{} ",
prompt_message(cformat!("Allow and remember? <bold>[y/N]</>"))
);
stderr().flush()?;
let mut response = String::new();
io::stdin().read_line(&mut response)?;
Ok(response.trim().eq_ignore_ascii_case("y"))
}
pub fn approve_alias_commands(
commands: &worktrunk::config::CommandConfig,
alias_name: &str,
project_id: &str,
yes: bool,
) -> anyhow::Result<bool> {
let approvals = Approvals::load().context("Failed to load approvals")?;
let cmds: Vec<_> = commands
.commands()
.map(|cmd| ApprovableCommand {
phase: Phase::Alias,
command: worktrunk::config::Command::new(
Some(cmd.name.clone().unwrap_or_else(|| alias_name.to_string())),
cmd.template.clone(),
),
})
.collect();
approve_command_batch(&cmds, project_id, &approvals, yes, false)
}
pub fn approve_hooks(
ctx: &super::command_executor::CommandContext<'_>,
hook_types: &[HookType],
) -> anyhow::Result<bool> {
approve_hooks_filtered(ctx, hook_types, &[])
}
pub fn approve_hooks_filtered(
ctx: &super::command_executor::CommandContext<'_>,
hook_types: &[HookType],
name_filters: &[String],
) -> anyhow::Result<bool> {
let parsed_filters: Vec<ParsedFilter<'_>> = name_filters
.iter()
.map(|f| ParsedFilter::parse(f))
.collect();
if !parsed_filters.is_empty()
&& parsed_filters
.iter()
.all(|f| f.source == Some(HookSource::User))
{
return Ok(true);
}
let project_config = match ctx.repo.load_project_config()? {
Some(cfg) => cfg,
None => return Ok(true), };
let mut commands = collect_commands_for_hooks(&project_config, hook_types);
let filter_names: Vec<&str> = parsed_filters
.iter()
.map(|f| f.name)
.filter(|n| !n.is_empty())
.collect();
if !filter_names.is_empty() {
commands.retain(|cmd| {
cmd.command
.name
.as_deref()
.is_some_and(|n| filter_names.contains(&n))
});
}
if commands.is_empty() {
return Ok(true);
}
let project_id = ctx.repo.project_identifier()?;
let approvals = Approvals::load().context("Failed to load approvals")?;
approve_command_batch(&commands, &project_id, &approvals, ctx.yes, false)
}