git_worktree_manager/operations/
run.rs1use std::io::Write;
4use std::path::Path;
5use std::process::Command;
6
7use crate::error::Result;
8use crate::scope;
9
10pub fn run_in_scope(
11 cwd: &Path,
12 cmd: &[String],
13 only: Option<&str>,
14 no_main: bool,
15 jobs: usize,
16 continue_on_error: bool,
17) -> Result<i32> {
18 let mut stdout = std::io::stdout().lock();
19 run_in_scope_to_writer(
20 cwd,
21 cmd,
22 only,
23 no_main,
24 jobs,
25 continue_on_error,
26 &mut stdout,
27 )
28}
29
30pub fn run_in_scope_to_writer<W: Write>(
34 cwd: &Path,
35 cmd: &[String],
36 only: Option<&str>,
37 no_main: bool,
38 jobs: usize,
39 continue_on_error: bool,
40 out: &mut W,
41) -> Result<i32> {
42 let scope = scope::discover_scope(cwd)?;
43 let mut targets: Vec<&scope::ScopedWorktree> = scope
44 .worktrees()
45 .iter()
46 .filter(|w| !no_main || !w.is_main)
47 .filter(|w| match only {
48 Some(g) => {
53 w.branch
54 .as_deref()
55 .map(|b| glob_match(g, b))
56 .unwrap_or(false)
57 || glob_match(g, &w.name)
58 }
59 None => true,
60 })
61 .collect();
62 targets.sort_by_key(|w| (!w.is_main, w.name.clone())); let jobs = jobs.max(1);
65 let mut last_failure: i32 = 0;
66 let mut i = 0;
67 while i < targets.len() {
68 let batch_end = (i + jobs).min(targets.len());
69
70 let handles: Vec<std::thread::JoinHandle<Result<(usize, String, i32)>>> = (i..batch_end)
73 .map(|k| {
74 let w = targets[k];
75 let name = w.name.clone();
76 let path = w.path.clone();
77 let cmd_v: Vec<String> = cmd.to_vec();
78 std::thread::spawn(move || {
79 let (s, code) = run_capture_one(&name, &path, &cmd_v)?;
80 Ok((k, s, code))
81 })
82 })
83 .collect();
84
85 let mut results: Vec<(usize, String, i32)> = handles
89 .into_iter()
90 .map(|h| h.join().expect("worker panicked"))
91 .collect::<Result<Vec<_>>>()?;
92 results.sort_by_key(|(k, _, _)| *k);
93
94 for (_, s, code) in results {
95 let _ = out.write_all(s.as_bytes());
96 if code != 0 {
97 last_failure = code;
98 if !continue_on_error {
99 return Ok(last_failure);
100 }
101 }
102 }
103 i = batch_end;
104 }
105 Ok(last_failure)
106}
107
108pub(crate) fn run_capture_one(name: &str, path: &Path, cmd: &[String]) -> Result<(String, i32)> {
109 if cmd.is_empty() {
110 return Err(crate::error::CwError::Other(
111 "run_capture_one: empty command".into(),
112 ));
113 }
114 let prefix = format!("[{}] ", name);
115 let mut child = Command::new(&cmd[0])
116 .args(&cmd[1..])
117 .current_dir(path)
118 .stdout(std::process::Stdio::piped())
119 .stderr(std::process::Stdio::piped())
120 .spawn()?;
121 let stdout = child.stdout.take().expect("piped stdout");
122 let stderr = child.stderr.take().expect("piped stderr");
123
124 let prefix_a = prefix.clone();
125 let h1 = std::thread::spawn(move || pipe_with_prefix(stdout, &prefix_a));
126 let prefix_b = prefix.clone();
127 let h2 = std::thread::spawn(move || pipe_with_prefix(stderr, &prefix_b));
128
129 let so = h1.join().unwrap_or_default();
130 let se = h2.join().unwrap_or_default();
131 let mut combined = so;
132 combined.push_str(&se);
133
134 let status = child.wait()?;
135 Ok((combined, status.code().unwrap_or(1)))
136}
137
138fn pipe_with_prefix<R: std::io::Read>(reader: R, prefix: &str) -> String {
139 use std::io::BufRead;
140 let buf = std::io::BufReader::new(reader);
141 let mut out = String::new();
142 for line in buf.lines().map_while(|l| l.ok()) {
143 out.push_str(prefix);
144 out.push_str(&line);
145 out.push('\n');
146 }
147 out
148}
149
150fn glob_match(pattern: &str, name: &str) -> bool {
151 if pattern.is_empty() {
153 return name.is_empty();
154 }
155 let parts: Vec<&str> = pattern.split('*').collect();
157 let mut idx = 0usize;
158 let last = parts.len() - 1;
159 if !pattern.starts_with('*') && !name.starts_with(parts[0]) {
160 return false;
161 }
162 for (i, part) in parts.iter().enumerate() {
163 if part.is_empty() {
164 continue;
165 }
166 let pos = if i == last && !pattern.ends_with('*') {
170 name[idx..].rfind(part)
171 } else {
172 name[idx..].find(part)
173 };
174 match pos {
175 Some(p) => idx += p + part.len(),
176 None => return false,
177 }
178 if i == last && !pattern.ends_with('*') && idx != name.len() {
179 return false;
180 }
181 }
182 true
183}
184
185#[cfg(test)]
186mod glob_tests {
187 use super::glob_match;
188
189 #[test]
190 fn empty_pattern_matches_only_empty() {
191 assert!(glob_match("", ""));
192 assert!(!glob_match("", "anything"));
193 }
194
195 #[test]
196 fn star_matches_anything() {
197 assert!(glob_match("*", ""));
198 assert!(glob_match("*", "anything"));
199 }
200
201 #[test]
202 fn literal_matches_exactly() {
203 assert!(glob_match("foo", "foo"));
204 assert!(!glob_match("foo", "foobar"));
205 assert!(!glob_match("foo", "barfoo"));
206 assert!(!glob_match("foo", "fo"));
207 }
208
209 #[test]
210 fn leading_star() {
211 assert!(glob_match("*foo", "foo"));
212 assert!(glob_match("*foo", "barfoo"));
213 assert!(!glob_match("*foo", "foobar"));
214 }
215
216 #[test]
217 fn trailing_star() {
218 assert!(glob_match("foo*", "foo"));
219 assert!(glob_match("foo*", "foobar"));
220 assert!(!glob_match("foo*", "barfoo"));
221 }
222
223 #[test]
224 fn double_star_collapses() {
225 assert!(glob_match("**foo", "foo"));
227 assert!(glob_match("foo**bar", "foobar"));
228 }
229
230 #[test]
231 fn middle_star() {
232 assert!(glob_match("a*b", "ab"));
233 assert!(glob_match("a*b", "axb"));
234 assert!(!glob_match("a*b", "axc"));
235 }
236
237 #[test]
238 fn trailing_literal_anchored_to_end_with_repeated_substring() {
239 assert!(glob_match("*a*a", "aaaa"));
241 assert!(glob_match("*a", "aaaa"));
242 assert!(!glob_match("*a", "aaab"));
243 }
244
245 #[test]
246 fn realistic_feat_glob() {
247 assert!(glob_match("feat-*", "feat-login"));
248 assert!(glob_match("feat-*", "feat-"));
249 assert!(!glob_match("feat-*", "bug-feat-x"));
250 }
251}