use crate::command::cmd::Cmd; use crate::command::context::GitPipeContext; use std::fmt; use std::path::PathBuf; #[derive(Clone)] pub struct Pipe { pub name: String, pub cmds: Vec, pub dir: PathBuf, pub timeout: Option, pub cancelled: bool, } impl Pipe { pub fn new(name: impl Into, dir: PathBuf) -> Self { Self { name: name.into(), cmds: Vec::new(), dir, timeout: None, cancelled: false, } } /// Whether new subprocesses may still be spawned on this pipe. /// Once `cancel_pipe()` has been called this returns `false` until `reset()`. pub fn spawn_allowed(&self) -> bool { !self.cancelled } /// Re-arm a cancelled pipe so new commands may be scheduled again. /// Pre-existing children that were killed during `cancel_pipe` are kept in /// `cmds` for audit but must not be reused. pub fn reset(&mut self) { self.cancelled = false; } pub fn push(mut self, cmd: Cmd) -> Self { self.cmds.push(cmd); self } pub fn extend>(mut self, cmds: I) -> Self { self.cmds.extend(cmds); self } /// Set the per-command timeout in **seconds** (matching `Cmd::timeout`, /// which is interpreted via `Duration::from_secs`). pub fn with_timeout(mut self, timeout_secs: u64) -> Self { self.timeout = Some(timeout_secs); self } pub fn with_dir(mut self, dir: PathBuf) -> Self { self.dir = dir; self } pub fn len(&self) -> usize { self.cmds.len() } pub fn is_empty(&self) -> bool { self.cmds.is_empty() } pub fn is_cancelled(&self) -> bool { self.cancelled } pub fn first(&self) -> Option<&Cmd> { self.cmds.first() } pub fn last(&self) -> Option<&Cmd> { self.cmds.last() } } impl fmt::Debug for Pipe { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Pipe") .field("name", &self.name) .field("cmds", &self.cmds.len()) .field("dir", &self.dir) .field("timeout", &self.timeout) .field("cancelled", &self.cancelled) .finish() } } impl fmt::Display for Pipe { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.cmds.is_empty() { return write!(f, "{}:", self.name); } let parts: Vec = self.cmds.iter().map(|c| format!("{c}")).collect(); write!(f, "{}: {}", self.name, parts.join(" | ")) } } impl GitPipeContext for Pipe { fn cancel_pipe(&mut self) { self.cancelled = true; for cmd in &mut self.cmds { if let Some(mut child) = cmd.cmd.take() { // The child handle is handed to the reaper task below; mark the // command as finished so it can never be spawned again, even // after a later `reset()` re-arms the pipe. cmd.finished = true; let _ = child.start_kill(); tokio::spawn(async move { let _ = child.wait().await; }); } } } fn current_index(&self) -> Option { if self.cancelled || self.cmds.is_empty() { None } else { Some(self.cmds.len().saturating_sub(1)) } } }