use crate::config::ExecConfig;
use crate::error::{GwmError, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExecStatus {
Ok,
Failed(i32),
Signal,
SpawnError(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExecOutcome {
pub name: String,
pub status: ExecStatus,
}
pub type CapturedRun = (ExecOutcome, Vec<u8>);
pub fn resolve_exec_command(profile: Option<&str>, inline: &[String], cfg: &ExecConfig) -> Result<Vec<String>> {
match (profile, inline.is_empty()) {
(Some(_), false) => Err(GwmError::Other(
"exec: --profile and an inline `-- <cmd>` are mutually exclusive — the profile carries the command".into(),
)),
(Some(name), true) => {
let p = cfg
.profiles
.get(name)
.ok_or_else(|| GwmError::Config(format!("exec: no profile named `{name}` in [exec.profiles]")))?;
validate_exec_profile_command(name, &p.command)?;
Ok(p.command.clone())
}
(None, false) => Ok(inline.to_vec()),
(None, true) => Err(GwmError::Other(
"exec: provide a command after `--` (e.g. `gwm exec -- cargo test`) or pass `--profile <name>`".into(),
)),
}
}
pub fn validate_exec_profile_command(profile: &str, command: &[String]) -> Result<()> {
if command.is_empty() {
return Err(GwmError::Config(format!(
"exec: profile `{profile}` has an empty `command` — give it an argv array like `command = [\"cargo\", \"test\"]`"
)));
}
Ok(())
}
pub fn exec_in_dir(dir: &Path, program: &str, args: &[String]) -> ExecStatus {
let resolved = resolve_program(dir, program);
match Command::new(&resolved).args(args).current_dir(dir).status() {
Ok(status) => match status.code() {
Some(0) => ExecStatus::Ok,
Some(code) => ExecStatus::Failed(code),
None => ExecStatus::Signal,
},
Err(e) => ExecStatus::SpawnError(e.to_string()),
}
}
pub fn resolve_jobs(flag: Option<u32>, profile: Option<&str>, cfg: &ExecConfig) -> usize {
let n = flag
.or_else(|| profile.and_then(|p| cfg.profiles.get(p)).and_then(|p| p.jobs))
.or(cfg.jobs)
.unwrap_or(1);
n.max(1) as usize
}
pub fn exec_capture_in_dir(dir: &Path, program: &str, args: &[String]) -> (ExecStatus, Vec<u8>) {
let resolved = resolve_program(dir, program);
match Command::new(&resolved).args(args).current_dir(dir).output() {
Ok(out) => {
let mut buf = out.stdout;
buf.extend_from_slice(&out.stderr);
let status = match out.status.code() {
Some(0) => ExecStatus::Ok,
Some(code) => ExecStatus::Failed(code),
None => ExecStatus::Signal,
};
(status, buf)
}
Err(e) => (ExecStatus::SpawnError(e.to_string()), Vec::new()),
}
}
pub fn run_in_dirs_parallel(
jobs: usize,
items: &[(String, PathBuf)],
program: &str,
args: &[String],
) -> Vec<CapturedRun> {
if items.is_empty() {
return Vec::new();
}
let workers = jobs.clamp(1, items.len());
let next = AtomicUsize::new(0);
let slots: Vec<Mutex<Option<CapturedRun>>> = (0..items.len()).map(|_| Mutex::new(None)).collect();
std::thread::scope(|s| {
for _ in 0..workers {
s.spawn(|| loop {
let i = next.fetch_add(1, Ordering::Relaxed);
if i >= items.len() {
break;
}
let (name, path) = &items[i];
let (status, output) = exec_capture_in_dir(path, program, args);
*slots[i].lock().expect("exec worker mutex never poisoned") = Some((
ExecOutcome {
name: name.clone(),
status,
},
output,
));
});
}
});
slots
.into_iter()
.map(|m| m.into_inner().expect("exec worker mutex never poisoned"))
.map(|slot| slot.expect("every worktree slot filled by a worker"))
.collect()
}
pub fn resolve_program(dir: &Path, program: &str) -> PathBuf {
let p = Path::new(program);
if p.is_relative() && has_path_separator(program) {
dir.join(p)
} else {
p.to_path_buf()
}
}
fn has_path_separator(program: &str) -> bool {
program.contains('/') || (cfg!(windows) && program.contains('\\'))
}
pub fn rollup_exit_code(outcomes: &[ExecOutcome]) -> i32 {
if outcomes.iter().all(|o| o.status == ExecStatus::Ok) {
0
} else {
1
}
}
pub fn format_outcome(o: &ExecOutcome) -> String {
match &o.status {
ExecStatus::Ok => format!("✓ {}", o.name),
ExecStatus::Failed(code) => format!("✗ {} (exit {})", o.name, code),
ExecStatus::Signal => format!("✗ {} (killed by signal)", o.name),
ExecStatus::SpawnError(msg) => format!("✗ {} (spawn error: {})", o.name, msg),
}
}