1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4use anyhow::{Result, anyhow, bail};
5use clap::ArgAction;
6
7use crate::stack;
8use crate::style;
9
10const SCRATCH_DIR: &str = "git-stk-run-worktree";
15
16#[derive(Debug, clap::Args)]
21pub struct Run {
22 #[arg(long, action = ArgAction::SetTrue)]
24 fail_fast: bool,
25 #[arg(long, action = ArgAction::SetTrue)]
30 no_worktree: bool,
31 #[arg(
33 trailing_var_arg = true,
34 allow_hyphen_values = true,
35 required = true,
36 num_args = 1..,
37 value_name = "CMD"
38 )]
39 command: Vec<String>,
40}
41
42impl crate::commands::Run for Run {
43 fn run(self) -> Result<()> {
44 let original = crate::git::current_branch()?;
45 let branches = stack::current_stack_branches(&original)?;
46
47 if branches.is_empty() {
48 bail!("no stacked branches to run on");
49 }
50
51 let (program, args) = self
52 .command
53 .split_first()
54 .expect("clap requires at least one command word");
55
56 let results = if self.no_worktree {
57 if !crate::git::worktree_is_clean()? {
60 bail!(
61 "working tree has uncommitted changes; commit or stash before \
62 `git stk run --no-worktree`"
63 );
64 }
65 let result = run_each_in_place(&branches, program, args, self.fail_fast);
68 let _ = crate::git::checkout(&original);
69 result?
70 } else {
71 let scratch = ScratchWorktree::create(&branches[0])?;
74 run_each_in(
75 scratch.path(),
76 &cwd_within_repo(),
77 &branches,
78 program,
79 args,
80 self.fail_fast,
81 )?
82 };
83
84 print_summary(&results);
85
86 if results.iter().any(|(_, passed)| !passed) {
87 bail!("`{program}` failed on one or more branches");
88 }
89 Ok(())
90 }
91}
92
93struct ScratchWorktree {
96 path: PathBuf,
97}
98
99impl ScratchWorktree {
100 fn create(commit: &str) -> Result<Self> {
101 let path = crate::git::git_common_path_absolute(SCRATCH_DIR)?;
102
103 if path.exists() {
107 let _ = crate::git::worktree_remove(&path);
108 let _ = std::fs::remove_dir_all(&path);
109 }
110
111 crate::git::worktree_add_detached(&path, commit)?;
112 Ok(Self { path })
113 }
114
115 fn path(&self) -> &Path {
116 &self.path
117 }
118}
119
120impl Drop for ScratchWorktree {
121 fn drop(&mut self) {
122 let _ = crate::git::worktree_remove(&self.path);
125 }
126}
127
128fn run_each_in(
131 worktree: &Path,
132 subdirectory: &Path,
133 branches: &[String],
134 program: &str,
135 args: &[String],
136 fail_fast: bool,
137) -> Result<Vec<(String, bool)>> {
138 let mut results = Vec::new();
139 for branch in branches {
140 crate::git::checkout_detached_in(worktree, branch)?;
141 anstream::println!("{}", style::branch(branch));
142 let dir = mirrored_dir(worktree, subdirectory);
144 let passed = run_once(&dir, program, args)?;
145 results.push((branch.clone(), passed));
146 if !passed && fail_fast {
147 break;
148 }
149 }
150 Ok(results)
151}
152
153fn run_each_in_place(
155 branches: &[String],
156 program: &str,
157 args: &[String],
158 fail_fast: bool,
159) -> Result<Vec<(String, bool)>> {
160 let here = std::env::current_dir().or_else(|_| crate::git::repo_root())?;
163 let mut results = Vec::new();
164 for branch in branches {
165 crate::git::checkout(branch)?;
166 anstream::println!("{}", style::branch(branch));
167 let passed = run_once(&here, program, args)?;
168 results.push((branch.clone(), passed));
169 if !passed && fail_fast {
170 break;
171 }
172 }
173 Ok(results)
174}
175
176fn cwd_within_repo() -> PathBuf {
180 let (Ok(cwd), Ok(root)) = (std::env::current_dir(), crate::git::repo_root()) else {
181 return PathBuf::new();
182 };
183 let cwd = cwd.canonicalize().unwrap_or(cwd);
186 let root = root.canonicalize().unwrap_or(root);
187 cwd.strip_prefix(&root)
188 .map(Path::to_path_buf)
189 .unwrap_or_default()
190}
191
192fn mirrored_dir(worktree: &Path, subdirectory: &Path) -> PathBuf {
196 let candidate = worktree.join(subdirectory);
197 if candidate.is_dir() {
198 candidate
199 } else {
200 worktree.to_path_buf()
201 }
202}
203
204fn run_once(dir: &Path, program: &str, args: &[String]) -> Result<bool> {
206 match Command::new(program).args(args).current_dir(dir).status() {
207 Ok(status) => Ok(status.success()),
208 Err(error) => Err(spawn_error(program, args, &error)),
213 }
214}
215
216fn spawn_error(program: &str, args: &[String], error: &std::io::Error) -> anyhow::Error {
221 let mut message = format!("failed to run `{program}`: {error}");
222 if args.is_empty() && program.split_whitespace().count() > 1 {
223 message.push_str(&format!(
224 "\nhint: pass the command unquoted after `--`, e.g. `git stk run -- {program}`"
225 ));
226 }
227 anyhow!(message)
228}
229
230fn print_summary(results: &[(String, bool)]) {
231 let width = results.iter().map(|(b, _)| b.len()).max().unwrap_or(0);
232 anstream::println!();
233 for (branch, passed) in results {
234 let pad = " ".repeat(width - branch.len());
235 let marker = if *passed {
236 style::success("ok")
237 } else {
238 style::paint(style::CLOSED, "FAIL")
239 };
240 anstream::println!(" {}{pad} {marker}", style::branch(branch));
241 }
242
243 let passed = results.iter().filter(|(_, passed)| *passed).count();
244 let total = results.len();
245 anstream::println!(
246 "{}",
247 style::dim(&format!(
248 "ran on {total} branch{}, {passed} passed",
249 if total == 1 { "" } else { "es" }
250 ))
251 );
252}