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 root = stack::stack_root(&original)?;
38 let trunk = stack::trunk_branch(&crate::git::local_branches()?);
39 let branches: Vec<String> = stack::branch_and_descendants(&root)?
40 .into_iter()
41 .filter(|branch| Some(branch) != trunk.as_ref())
42 .collect();
43
44 if branches.is_empty() {
45 bail!("no stacked branches to run on");
46 }
47
48 let (program, args) = self
49 .command
50 .split_first()
51 .expect("clap requires at least one command word");
52
53 let result = run_each(&branches, program, args, self.fail_fast);
56 let _ = crate::git::checkout(&original);
57 let results = result?;
58
59 print_summary(&results);
60
61 if results.iter().any(|(_, passed)| !passed) {
62 bail!("`{program}` failed on one or more branches");
63 }
64 Ok(())
65 }
66}
67
68fn run_each(
70 branches: &[String],
71 program: &str,
72 args: &[String],
73 fail_fast: bool,
74) -> Result<Vec<(String, bool)>> {
75 let mut results = Vec::new();
76 for branch in branches {
77 crate::git::checkout(branch)?;
78 anstream::println!("{}", style::branch(branch));
79 let passed = Command::new(program)
81 .args(args)
82 .status()
83 .is_ok_and(|status| status.success());
84 results.push((branch.clone(), passed));
85 if !passed && fail_fast {
86 break;
87 }
88 }
89 Ok(results)
90}
91
92fn print_summary(results: &[(String, bool)]) {
93 let width = results.iter().map(|(b, _)| b.len()).max().unwrap_or(0);
94 anstream::println!();
95 for (branch, passed) in results {
96 let pad = " ".repeat(width - branch.len());
97 let marker = if *passed {
98 style::success("ok")
99 } else {
100 style::paint(style::CLOSED, "FAIL")
101 };
102 anstream::println!(" {}{pad} {marker}", style::branch(branch));
103 }
104
105 let passed = results.iter().filter(|(_, passed)| *passed).count();
106 let total = results.len();
107 anstream::println!(
108 "{}",
109 style::dim(&format!(
110 "ran on {total} branch{}, {passed} passed",
111 if total == 1 { "" } else { "es" }
112 ))
113 );
114}