1use std::process::Command;
2
3use anyhow::{Result, anyhow, 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 = match Command::new(program).args(args).status() {
77 Ok(status) => status.success(),
78 Err(error) => return Err(spawn_error(program, args, &error)),
83 };
84 results.push((branch.clone(), passed));
85 if !passed && fail_fast {
86 break;
87 }
88 }
89 Ok(results)
90}
91
92fn spawn_error(program: &str, args: &[String], error: &std::io::Error) -> anyhow::Error {
97 let mut message = format!("failed to run `{program}`: {error}");
98 if args.is_empty() && program.split_whitespace().count() > 1 {
99 message.push_str(&format!(
100 "\nhint: pass the command unquoted after `--`, e.g. `git stk run -- {program}`"
101 ));
102 }
103 anyhow!(message)
104}
105
106fn print_summary(results: &[(String, bool)]) {
107 let width = results.iter().map(|(b, _)| b.len()).max().unwrap_or(0);
108 anstream::println!();
109 for (branch, passed) in results {
110 let pad = " ".repeat(width - branch.len());
111 let marker = if *passed {
112 style::success("ok")
113 } else {
114 style::paint(style::CLOSED, "FAIL")
115 };
116 anstream::println!(" {}{pad} {marker}", style::branch(branch));
117 }
118
119 let passed = results.iter().filter(|(_, passed)| *passed).count();
120 let total = results.len();
121 anstream::println!(
122 "{}",
123 style::dim(&format!(
124 "ran on {total} branch{}, {passed} passed",
125 if total == 1 { "" } else { "es" }
126 ))
127 );
128}