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, and report a
10/// per-branch pass/fail summary. Answers "does each layer build on its own?"
11/// before submitting - each PR is supposed to be independently green.
12#[derive(Debug, clap::Args)]
13pub struct Run {
14    /// Stop at the first branch whose command fails.
15    #[arg(long, action = ArgAction::SetTrue)]
16    fail_fast: bool,
17    /// The command to run on each branch (everything after `--`).
18    #[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        // Switching branches with uncommitted changes would drag them across
31        // the stack or fail outright; require a clean tree.
32        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        // Always return to where we started, even if a checkout or the
54        // command errors partway through.
55        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
68/// Check out each branch in turn and run the command, collecting pass/fail.
69fn 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        // Inherit stdio so the command's output streams through live.
80        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}