1use std::process::Command;
2
3use anyhow::{Result, bail};
4use clap::ArgAction;
5
6use crate::stack;
7use crate::style;
8
9#[derive(Debug, clap::Args)]
14pub struct Run {
15 #[arg(long, action = ArgAction::SetTrue)]
17 fail_fast: bool,
18 #[arg(
20 trailing_var_arg = true,
21 allow_hyphen_values = true,
22 required = true,
23 num_args = 1..,
24 value_name = "CMD"
25 )]
26 command: Vec<String>,
27}
28
29impl crate::commands::Run for Run {
30 fn run(self) -> Result<()> {
31 if !crate::git::worktree_is_clean()? {
34 bail!("working tree has uncommitted changes; commit or stash before `git stk run`");
35 }
36
37 let original = crate::git::current_branch()?;
38 let branches = stack::current_stack_branches(&original)?;
39
40 if branches.is_empty() {
41 bail!("no stacked branches to run on");
42 }
43
44 let (program, args) = self
45 .command
46 .split_first()
47 .expect("clap requires at least one command word");
48
49 let result = run_each(&branches, program, args, self.fail_fast);
52 let _ = crate::git::checkout(&original);
53 let results = result?;
54
55 print_summary(&results);
56
57 if results.iter().any(|(_, passed)| !passed) {
58 bail!("`{program}` failed on one or more branches");
59 }
60 Ok(())
61 }
62}
63
64fn run_each(
66 branches: &[String],
67 program: &str,
68 args: &[String],
69 fail_fast: bool,
70) -> Result<Vec<(String, bool)>> {
71 let mut results = Vec::new();
72 for branch in branches {
73 crate::git::checkout(branch)?;
74 anstream::println!("{}", style::branch(branch));
75 let passed = Command::new(program)
77 .args(args)
78 .status()
79 .is_ok_and(|status| status.success());
80 results.push((branch.clone(), passed));
81 if !passed && fail_fast {
82 break;
83 }
84 }
85 Ok(results)
86}
87
88fn print_summary(results: &[(String, bool)]) {
89 let width = results.iter().map(|(b, _)| b.len()).max().unwrap_or(0);
90 anstream::println!();
91 for (branch, passed) in results {
92 let pad = " ".repeat(width - branch.len());
93 let marker = if *passed {
94 style::success("ok")
95 } else {
96 style::paint(style::CLOSED, "FAIL")
97 };
98 anstream::println!(" {}{pad} {marker}", style::branch(branch));
99 }
100
101 let passed = results.iter().filter(|(_, passed)| *passed).count();
102 let total = results.len();
103 anstream::println!(
104 "{}",
105 style::dim(&format!(
106 "ran on {total} branch{}, {passed} passed",
107 if total == 1 { "" } else { "es" }
108 ))
109 );
110}