use std::collections::HashMap;
use std::io::Write;
use std::process::Stdio;
use color_print::cformat;
use worktrunk::config::{UserConfig, expand_template};
use worktrunk::git::Repository;
use worktrunk::git::WorktrunkError;
use worktrunk::shell_exec::ShellConfig;
use worktrunk::styling::{
eprintln, error_message, format_with_gutter, progress_message, success_message, warning_message,
};
use crate::commands::command_executor::{CommandContext, build_hook_context};
use crate::commands::worktree_display_name;
pub fn step_for_each(args: Vec<String>) -> anyhow::Result<()> {
let repo = Repository::current()?;
let worktrees: Vec<_> = repo
.list_worktrees()?
.into_iter()
.filter(|wt| !wt.is_prunable())
.collect();
let config = UserConfig::load()?;
let mut failed: Vec<String> = Vec::new();
let total = worktrees.len();
let command_template = args.join(" ");
for wt in &worktrees {
let display_name = worktree_display_name(wt, &repo, &config);
eprintln!(
"{}",
progress_message(format!("Running in {display_name}..."))
);
let ctx = CommandContext::new(&repo, &config, wt.branch.as_deref(), &wt.path, false);
let context_map = build_hook_context(&ctx, &[])?;
let vars: HashMap<&str, &str> = context_map
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let command = expand_template(&command_template, &vars, true, &repo, "for-each command")?;
let context_json = serde_json::to_string(&context_map)
.expect("HashMap<String, String> serialization should never fail");
match run_command_streaming(&command, &wt.path, Some(&context_json)) {
Ok(()) => {}
Err(CommandError::SpawnFailed(err)) => {
eprintln!(
"{}",
error_message(cformat!("Failed in <bold>{display_name}</> (spawn failed)"))
);
eprintln!("{}", format_with_gutter(&err, None));
failed.push(display_name.to_string());
}
Err(CommandError::ExitCode(exit_code)) => {
let exit_info = exit_code
.map(|code| format!(" (exit code {code})"))
.unwrap_or_default();
eprintln!(
"{}",
error_message(cformat!("Failed in <bold>{display_name}</>{exit_info}"))
);
failed.push(display_name.to_string());
}
}
}
eprintln!();
if failed.is_empty() {
eprintln!(
"{}",
success_message(format!(
"Completed in {total} worktree{}",
if total == 1 { "" } else { "s" }
))
);
Ok(())
} else {
eprintln!(
"{}",
warning_message(format!(
"{} of {total} worktree{} failed",
failed.len(),
if total == 1 { "" } else { "s" }
))
);
let failed_list = failed.join("\n");
eprintln!("{}", format_with_gutter(&failed_list, None));
Err(WorktrunkError::AlreadyDisplayed { exit_code: 1 }.into())
}
}
pub(crate) enum CommandError {
SpawnFailed(String),
ExitCode(Option<i32>),
}
pub(crate) fn run_command_streaming(
command: &str,
working_dir: &std::path::Path,
stdin_content: Option<&str>,
) -> Result<(), CommandError> {
let shell = ShellConfig::get().map_err(|e| CommandError::SpawnFailed(e.to_string()))?;
log::debug!("$ {} (streaming)", command);
let stdin_mode = if stdin_content.is_some() {
Stdio::piped()
} else {
Stdio::inherit() };
let mut child = shell
.command(command)
.current_dir(working_dir)
.stdin(stdin_mode)
.stdout(Stdio::from(std::io::stderr()))
.stderr(Stdio::inherit())
.env_remove(worktrunk::shell_exec::DIRECTIVE_FILE_ENV_VAR)
.spawn()
.map_err(|e| CommandError::SpawnFailed(e.to_string()))?;
if let Some(content) = stdin_content
&& let Some(mut stdin) = child.stdin.take()
{
let _ = stdin.write_all(content.as_bytes());
}
let status = child
.wait()
.map_err(|e| CommandError::SpawnFailed(e.to_string()))?;
if status.success() {
Ok(())
} else {
Err(CommandError::ExitCode(status.code()))
}
}