Skip to main content

git_stk/commands/
run.rs

1use std::process::Command;
2
3use anyhow::{Result, bail};
4use clap::ArgAction;
5
6use crate::stack;
7use crate::style;
8
9/// Run a command on every branch in the stack, bottom-up, with a pass/fail summary.
10///
11/// Answers "does each layer build on its own?" before submitting - each PR is
12/// supposed to be independently green.
13#[derive(Debug, clap::Args)]
14pub struct Run {
15    /// Stop at the first branch whose command fails.
16    #[arg(long, action = ArgAction::SetTrue)]
17    fail_fast: bool,
18    /// The command to run on each branch (everything after `--`).
19    #[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        // Switching branches with uncommitted changes would drag them across
32        // the stack or fail outright; require a clean tree.
33        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        // Always return to where we started, even if a checkout or the
50        // command errors partway through.
51        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
64/// Check out each branch in turn and run the command, collecting pass/fail.
65fn 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        // Inherit stdio so the command's output streams through live.
76        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}