use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Result, anyhow, bail};
use clap::ArgAction;
use crate::stack;
use crate::style;
const SCRATCH_DIR: &str = "git-stk-run-worktree";
#[derive(Debug, clap::Args)]
pub struct Run {
#[arg(long, action = ArgAction::SetTrue)]
fail_fast: bool,
#[arg(long, action = ArgAction::SetTrue)]
no_worktree: bool,
#[arg(
trailing_var_arg = true,
allow_hyphen_values = true,
required = true,
num_args = 1..,
value_name = "CMD"
)]
command: Vec<String>,
}
impl crate::commands::Run for Run {
fn run(self) -> Result<()> {
let original = crate::git::current_branch()?;
let branches = stack::current_stack_branches(&original)?;
if branches.is_empty() {
bail!("no stacked branches to run on");
}
let (program, args) = self
.command
.split_first()
.expect("clap requires at least one command word");
let results = if self.no_worktree {
if !crate::git::worktree_is_clean()? {
bail!(
"working tree has uncommitted changes; commit or stash before \
`git stk run --no-worktree`"
);
}
let result = run_each_in_place(&branches, program, args, self.fail_fast);
let _ = crate::git::checkout(&original);
result?
} else {
let scratch = ScratchWorktree::create(&branches[0])?;
run_each_in(
scratch.path(),
&cwd_within_repo(),
&branches,
program,
args,
self.fail_fast,
)?
};
print_summary(&results);
if results.iter().any(|(_, passed)| !passed) {
bail!("`{program}` failed on one or more branches");
}
Ok(())
}
}
struct ScratchWorktree {
path: PathBuf,
}
impl ScratchWorktree {
fn create(commit: &str) -> Result<Self> {
let path = crate::git::git_common_path_absolute(SCRATCH_DIR)?;
if path.exists() {
let _ = crate::git::worktree_remove(&path);
let _ = std::fs::remove_dir_all(&path);
}
crate::git::worktree_add_detached(&path, commit)?;
Ok(Self { path })
}
fn path(&self) -> &Path {
&self.path
}
}
impl Drop for ScratchWorktree {
fn drop(&mut self) {
let _ = crate::git::worktree_remove(&self.path);
}
}
fn run_each_in(
worktree: &Path,
subdirectory: &Path,
branches: &[String],
program: &str,
args: &[String],
fail_fast: bool,
) -> Result<Vec<(String, bool)>> {
let mut results = Vec::new();
for branch in branches {
crate::git::checkout_detached_in(worktree, branch)?;
anstream::println!("{}", style::branch(branch));
let dir = mirrored_dir(worktree, subdirectory);
let passed = run_once(&dir, program, args)?;
results.push((branch.clone(), passed));
if !passed && fail_fast {
break;
}
}
Ok(results)
}
fn run_each_in_place(
branches: &[String],
program: &str,
args: &[String],
fail_fast: bool,
) -> Result<Vec<(String, bool)>> {
let here = std::env::current_dir().or_else(|_| crate::git::repo_root())?;
let mut results = Vec::new();
for branch in branches {
crate::git::checkout(branch)?;
anstream::println!("{}", style::branch(branch));
let passed = run_once(&here, program, args)?;
results.push((branch.clone(), passed));
if !passed && fail_fast {
break;
}
}
Ok(results)
}
fn cwd_within_repo() -> PathBuf {
let (Ok(cwd), Ok(root)) = (std::env::current_dir(), crate::git::repo_root()) else {
return PathBuf::new();
};
let cwd = cwd.canonicalize().unwrap_or(cwd);
let root = root.canonicalize().unwrap_or(root);
cwd.strip_prefix(&root)
.map(Path::to_path_buf)
.unwrap_or_default()
}
fn mirrored_dir(worktree: &Path, subdirectory: &Path) -> PathBuf {
let candidate = worktree.join(subdirectory);
if candidate.is_dir() {
candidate
} else {
worktree.to_path_buf()
}
}
fn run_once(dir: &Path, program: &str, args: &[String]) -> Result<bool> {
match Command::new(program).args(args).current_dir(dir).status() {
Ok(status) => Ok(status.success()),
Err(error) => Err(spawn_error(program, args, &error)),
}
}
fn spawn_error(program: &str, args: &[String], error: &std::io::Error) -> anyhow::Error {
let mut message = format!("failed to run `{program}`: {error}");
if args.is_empty() && program.split_whitespace().count() > 1 {
message.push_str(&format!(
"\nhint: pass the command unquoted after `--`, e.g. `git stk run -- {program}`"
));
}
anyhow!(message)
}
fn print_summary(results: &[(String, bool)]) {
let width = results.iter().map(|(b, _)| b.len()).max().unwrap_or(0);
anstream::println!();
for (branch, passed) in results {
let pad = " ".repeat(width - branch.len());
let marker = if *passed {
style::success("ok")
} else {
style::paint(style::CLOSED, "FAIL")
};
anstream::println!(" {}{pad} {marker}", style::branch(branch));
}
let passed = results.iter().filter(|(_, passed)| *passed).count();
let total = results.len();
anstream::println!(
"{}",
style::dim(&format!(
"ran on {total} branch{}, {passed} passed",
if total == 1 { "" } else { "es" }
))
);
}