use std::{io, process::Command};
use thiserror::Error;
use crate::tracing::LogResult as _;
pub trait Backend {
fn call(&self, commit_message: &str) -> Result<(), BackendError>;
}
#[derive(Debug, Error)]
pub enum BackendError {
#[error("Failed to run `{command}`")]
CannotRun {
command: String,
os_error: io::Error,
},
#[error("The backend command has returned an error")]
ExecutionError {
status_code: Option<i32>,
},
}
pub struct GitBackend {
extra_args: Vec<String>,
}
impl GitBackend {
#[tracing::instrument(name = "new_git_backend", level = "trace", skip_all)]
pub fn new(extra_args: &[String]) -> Self {
Self {
extra_args: extra_args.to_owned(),
}
}
}
impl Backend for GitBackend {
#[tracing::instrument(name = "git_backend", level = "trace", skip_all)]
fn call(&self, commit_message: &str) -> Result<(), BackendError> {
let mut git_commit = Command::new("git");
git_commit.arg("commit");
#[cfg(feature = "unstable-pre-commit")]
git_commit.arg("--no-verify");
git_commit
.args(&self.extra_args)
.args(["-em", commit_message]);
tracing::info!(?git_commit, "calling git commit");
let status = git_commit
.status()
.map_err(|os_error| BackendError::CannotRun {
command: String::from("git commit"),
os_error,
})
.log_err()?;
tracing::debug!(?status);
if !status.success() {
Err(BackendError::ExecutionError {
status_code: status.code(),
})
.log_err()?;
}
Ok(())
}
}
pub struct CustomCommandBackend {
command: String,
args: Vec<String>,
}
#[derive(Debug, Error)]
pub enum CustomCommandBackendError {
#[error("Failed to parse `{command}`")]
Syntax {
command: String,
parse_error: shell_words::ParseError,
},
}
impl CustomCommandBackend {
#[expect(
clippy::unwrap_in_result,
reason = "The expect in the function cannot actually panic."
)]
#[tracing::instrument(
name = "new_custom_command_backend",
level = "trace",
skip_all
)]
pub fn new(command: &str) -> Result<Self, CustomCommandBackendError> {
let command_line: Vec<_> = shell_words::split(command)
.map_err(|parse_error| CustomCommandBackendError::Syntax {
command: command.to_owned(),
parse_error,
})
.log_err()?;
#[expect(
clippy::expect_used,
reason = "clap ensures `command` is non empty"
)]
let (command, args) =
command_line.split_first().expect("the command is empty");
Ok(Self {
command: command.to_owned(),
args: args.to_owned(),
})
}
}
impl Backend for CustomCommandBackend {
#[tracing::instrument(
name = "custom_command_backend",
level = "trace",
skip_all
)]
fn call(&self, commit_message: &str) -> Result<(), BackendError> {
let mut custom_command = Command::new(&self.command);
custom_command.args(embed_message_in_args(&self.args, commit_message));
tracing::info!(?custom_command, "calling a custom command");
let status = custom_command
.status()
.map_err(|os_error| BackendError::CannotRun {
command: format!("{} {}", &self.command, self.args.join(" "))
.trim()
.to_owned(),
os_error,
})
.log_err()?;
tracing::debug!(?status);
if !status.success() {
Err(BackendError::ExecutionError {
status_code: status.code(),
})
.log_err()?;
}
Ok(())
}
}
fn embed_message_in_args(args: &[String], commit_message: &str) -> Vec<String> {
args.iter()
.map(|arg| arg.replace("$message", commit_message))
.collect()
}
pub struct PrintBackend;
impl Backend for PrintBackend {
#[tracing::instrument(name = "print_backend", level = "trace", skip_all)]
fn call(&self, commit_message: &str) -> Result<(), BackendError> {
tracing::debug!("printing the commit message");
println!("{commit_message}");
Ok(())
}
}