#[cfg(any(target_os = "macos", target_os = "linux"))]
use std::cell::Cell;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::Command;
use super::PushFailure;
use crate::refs::RefName;
use crate::transport::TransportControl;
use crate::{ObjectId, Repository};
pub(super) struct HookUpdate<'a> {
pub(super) old: ObjectId,
pub(super) new: ObjectId,
pub(super) name: &'a RefName,
}
pub(super) struct ReceiveHooks {
hooks: crate::hooks::Hooks,
git_dir: PathBuf,
push_options: Vec<Vec<u8>>,
#[cfg(any(target_os = "macos", target_os = "linux"))]
output_remaining: Cell<usize>,
}
impl ReceiveHooks {
pub(super) fn new(
repository: &Repository,
push_options: Vec<Vec<u8>>,
output_limit: usize,
) -> Result<Self, PushFailure> {
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
let _ = output_limit;
let hooks = crate::hooks::Hooks::new(repository, &Default::default()).map_err(|_| {
PushFailure::Unsupported("core.hooksPath with ~ expansion in the receiving repository")
})?;
Ok(Self {
hooks,
git_dir: repository.git_dir().to_owned(),
push_options,
#[cfg(any(target_os = "macos", target_os = "linux"))]
output_remaining: Cell::new(output_limit),
})
}
pub(super) fn has(&self, name: &str) -> bool {
self.path(name).is_some()
}
fn path(&self, name: &str) -> Option<PathBuf> {
match self.hooks.find(name) {
crate::hooks::Found::Executable(path) => Some(path),
crate::hooks::Found::Absent | crate::hooks::Found::NotExecutable(_) => None,
}
}
pub(super) fn run(
&self,
name: &str,
arguments: &[&[u8]],
updates: &[HookUpdate<'_>],
control: TransportControl<'_>,
quarantine: Option<&Path>,
primary: &Path,
) -> Result<(bool, Vec<u8>), PushFailure> {
control.check()?;
let Some(path) = self.path(name) else {
return Ok((true, Vec::new()));
};
let mut command = Command::new(path);
command
.args(arguments.iter().map(|argument| os_string(argument)))
.current_dir(&self.git_dir)
.env("GIT_DIR", &self.git_dir)
.env("GIT_PUSH_OPTION_COUNT", self.push_options.len().to_string())
.env_remove("GIT_OBJECT_DIRECTORY")
.env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES")
.env_remove("GIT_QUARANTINE_PATH");
if let Some(directory) = quarantine {
command
.env("GIT_OBJECT_DIRECTORY", directory)
.env("GIT_QUARANTINE_PATH", directory)
.env(
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
super::quarantine::quoted_path(primary),
);
}
for (index, option) in self.push_options.iter().enumerate() {
command.env(format!("GIT_PUSH_OPTION_{index}"), os_string(option));
}
let mut input = Vec::new();
for update in updates {
input.extend_from_slice(format!("{} {} ", update.old, update.new).as_bytes());
input.extend_from_slice(update.name.as_bytes());
input.push(b'\n');
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
{
let (status, output) = crate::hooks::process::run(
&mut command,
&input,
control,
self.output_remaining.get(),
)
.map_err(PushFailure::from)?;
let output = (status.success(), output);
self.output_remaining
.set(self.output_remaining.get() - output.1.len());
Ok(output)
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
let _ = (command, input);
Err(PushFailure::Unsupported(
"receive hook execution on this platform",
))
}
}
}
#[cfg(unix)]
fn os_string(bytes: &[u8]) -> OsString {
use std::os::unix::ffi::OsStringExt as _;
OsString::from_vec(bytes.to_vec())
}
#[cfg(not(unix))]
fn os_string(bytes: &[u8]) -> OsString {
String::from_utf8_lossy(bytes).into_owned().into()
}