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