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 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        // Always return to where we started, even if a checkout or the
49        // command errors partway through.
50        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
63/// Check out each branch in turn and run the command, collecting pass/fail.
64fn 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        // Inherit stdio so the command's output streams through live.
75        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}