use std::ffi::OsStr;
use std::ops::Deref;
use std::path::Path;
use tempfile::TempDir;
use crate::config::Shell;
use crate::hook::Error;
use crate::languages::resolve_command;
use crate::store::Store;
#[derive(Debug)]
pub(crate) struct PreparedHookEntry {
argv: Vec<String>,
_temp_dir: Option<TempDir>,
}
impl PreparedHookEntry {
fn direct(argv: Vec<String>) -> Self {
Self {
argv,
_temp_dir: None,
}
}
fn shell(argv: Vec<String>, temp_dir: TempDir) -> Self {
Self {
argv,
_temp_dir: Some(temp_dir),
}
}
pub(crate) fn argv(&self) -> &[String] {
&self.argv
}
}
impl Deref for PreparedHookEntry {
type Target = [String];
fn deref(&self) -> &Self::Target {
&self.argv
}
}
#[derive(Debug, Clone)]
pub(crate) enum HookEntry {
Direct(DirectHookEntry),
Shell(ShellHookEntry),
}
impl HookEntry {
pub(crate) fn new(hook: String, entry: String, shell: Option<Shell>) -> Self {
match shell {
Some(shell) => Self::Shell(ShellHookEntry { hook, entry, shell }),
None => Self::Direct(DirectHookEntry { hook, entry }),
}
}
pub(crate) fn resolve(
&self,
env_path: Option<&OsStr>,
store: &Store,
) -> Result<PreparedHookEntry, Error> {
match self {
Self::Direct(entry) => entry.resolve(env_path),
Self::Shell(entry) => entry.resolve(env_path, store),
}
}
pub(crate) fn resolve_script(
&self,
repo_path: &Path,
env_path: Option<&OsStr>,
store: &Store,
) -> Result<PreparedHookEntry, Error> {
match self {
Self::Direct(entry) => entry.resolve_script(repo_path, env_path),
Self::Shell(entry) => entry.resolve(env_path, store),
}
}
pub(crate) fn expect_direct(&self) -> &DirectHookEntry {
match self {
Self::Direct(entry) => entry,
Self::Shell(entry) => {
panic!(
"Hook `{}` specified `shell`, but this execution path requires an argv-style entry",
entry.hook,
);
}
}
}
pub(crate) fn shell(&self) -> Option<Shell> {
match self {
Self::Direct(_) => None,
Self::Shell(entry) => Some(entry.shell),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct DirectHookEntry {
hook: String,
entry: String,
}
impl DirectHookEntry {
fn resolve(&self, env_path: Option<&OsStr>) -> Result<PreparedHookEntry, Error> {
let split = self.split()?;
Ok(PreparedHookEntry::direct(resolve_command(split, env_path)))
}
fn resolve_script(
&self,
repo_path: &Path,
env_path: Option<&OsStr>,
) -> Result<PreparedHookEntry, Error> {
let mut split = self.split()?;
let cmd = repo_path.join(&split[0]);
split[0] = cmd.to_string_lossy().to_string();
Ok(PreparedHookEntry::direct(resolve_command(split, env_path)))
}
pub(crate) fn split(&self) -> Result<Vec<String>, Error> {
let splits = shlex::split(&self.entry).ok_or_else(|| Error::Hook {
hook: self.hook.clone(),
error: anyhow::anyhow!("Failed to parse entry `{}` as commands", &self.entry),
})?;
if splits.is_empty() {
return Err(Error::Hook {
hook: self.hook.clone(),
error: anyhow::anyhow!("Failed to parse entry: entry is empty"),
});
}
Ok(splits)
}
pub(crate) fn raw(&self) -> &str {
&self.entry
}
}
#[derive(Debug, Clone)]
pub(crate) struct ShellHookEntry {
hook: String,
entry: String,
shell: Shell,
}
impl ShellHookEntry {
fn resolve(&self, env_path: Option<&OsStr>, store: &Store) -> Result<PreparedHookEntry, Error> {
let temp_dir = tempfile::tempdir_in(store.scratch_path())?;
let script_path = temp_dir
.path()
.join("entry")
.with_extension(self.shell.extension());
fs_err::write(&script_path, &self.entry).map_err(|err| Error::Hook {
hook: self.hook.clone(),
error: anyhow::anyhow!(err).context("Failed to write shell entry script"),
})?;
let argv = resolve_command(self.shell.argv_for_script(&script_path), env_path);
Ok(PreparedHookEntry::shell(argv, temp_dir))
}
}
impl Shell {
fn extension(self) -> &'static str {
match self {
Self::Sh | Self::Bash => "sh",
Self::Pwsh | Self::Powershell => "ps1",
Self::Cmd => "cmd",
}
}
fn argv_for_script(self, script_path: &Path) -> Vec<String> {
let script = script_path.to_string_lossy().to_string();
match self {
Self::Sh => vec!["sh".to_string(), "-e".to_string(), script],
Self::Bash => bash_argv(script),
Self::Pwsh => powershell_argv("pwsh", script),
Self::Powershell => powershell_argv("powershell", script),
Self::Cmd => cmd_argv(script),
}
}
}
fn bash_argv(script: String) -> Vec<String> {
const BASH_ARGV_PREFIX: &[&str] = &["bash", "--noprofile", "--norc", "-eo", "pipefail"];
let mut argv = BASH_ARGV_PREFIX
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>();
argv.push(script);
argv
}
fn powershell_argv(command: &str, script: String) -> Vec<String> {
let mut argv = vec![
command.to_string(),
"-NoProfile".to_string(),
"-NonInteractive".to_string(),
];
#[cfg(windows)]
argv.extend(["-ExecutionPolicy".to_string(), "Bypass".to_string()]);
argv.extend(["-File".to_string(), script]);
argv
}
fn cmd_argv(script: String) -> Vec<String> {
const CMD_ARGV_PREFIX: &[&str] = &["cmd", "/D", "/E:ON", "/V:OFF", "/S", "/C", "CALL"];
let mut argv = CMD_ARGV_PREFIX
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>();
argv.push(script);
argv
}