use std::io::{BufRead as _, BufReader};
use std::path::Path;
use std::process::{Command, Stdio};
use color_eyre::eyre::Result;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum HookError {
#[error("IO Error")]
IoError(#[from] std::io::Error),
#[error("Process failed with status `{0}`")]
ExitStatusError(std::process::ExitStatus),
}
impl From<std::process::ExitStatus> for HookError {
fn from(value: std::process::ExitStatus) -> Self {
Self::ExitStatusError(value)
}
}
trait ExitOk {
type Error;
fn exit_ok(self) -> Result<(), Self::Error>;
}
impl ExitOk for std::process::ExitStatus {
type Error = HookError;
fn exit_ok(self) -> Result<(), <Self as ExitOk>::Error> {
if self.success() {
Ok(())
} else {
Err(self.into())
}
}
}
#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Hook(String);
impl Hook {
pub fn new<S: Into<String>>(command: S) -> Self {
Self(command.into())
}
pub fn command(&self) -> &str {
&self.0
}
pub fn execute(&self, cwd: &Path) -> Result<()> {
let mut child = self
.prepare_command()?
.current_dir(cwd)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let stdout = child.stdout.take().expect("Failed to get stdout from hook");
for line in BufReader::new(stdout).lines() {
match line {
Ok(line) => log::info!("hook::stdout > {}", line),
Err(err) => {
let _ = child.kill();
return Err(err.into());
}
}
}
let stderr = child.stderr.take().expect("Failed to get stderr from hook");
for line in BufReader::new(stderr).lines() {
match line {
Ok(line) => log::error!("hook::stderr > {}", line),
Err(err) => {
let _ = child.kill();
return Err(err.into());
}
}
}
child
.wait_with_output()?
.status
.exit_ok()
.map_err(Into::into)
}
fn prepare_command(&self) -> Result<Command> {
cfg_if::cfg_if! {
if #[cfg(target_family = "windows")] {
let mut cmd = Command::new("cmd");
cmd.args(["/C", &self.0]);
Ok(cmd)
} else if #[cfg(target_family = "unix")] {
let mut cmd = Command::new("sh");
cmd.args(["-c", &self.0]);
Ok(cmd)
} else {
Err(std::io::Error::new(std::io::ErrorKind::Other, "Hooks are only supported on Windows and Unix-based systems"))
}
}
}
}