f082eb8
//! hook usecase: implements HookOps for GitRepository.
//!
//! Hooks are external to bare git operations; here we offer a default
//! implementation that always accepts (no-op) and a helper to run
//! arbitrary scripts so callers can plug in their policy engine.
use crate::GitRepository;
use crate::hook::impls::HookOps;
use crate::hook::types::{HookEvent, HookEventKind, HookRef, HookResult, ScriptOutput};
impl HookOps for GitRepository {
fn hook_pre_receive(&self, event: &HookEvent) -> Result<HookResult, crate::Error> {
// Fail-closed: without a configured policy engine, reject the push
// rather than silently accepting every ref update.
tracing::warn!(repo_id = event.repo_id, actor = ?event.actor, "pre-receive rejected: no hook policy configured");
Ok(HookResult {
accepted: false,
message: Some("rejected: no hook policy configured".to_string()),
})
}
fn hook_post_receive(&self, event: &HookEvent) -> Result<HookResult, crate::Error> {
// Post-receive is not an authorization gate; report it but do not
// fabricate acceptance.
tracing::info!(repo_id = event.repo_id, actor = ?event.actor, "post-receive event recorded");
Ok(HookResult {
accepted: true,
message: None,
})
}
fn hook_emit(&self, event: &HookEvent) -> Result<(), crate::Error> {
tracing::info!(repo_id = event.repo_id, actor = ?event.actor, kind = ?event.kind, "hook event emitted");
Ok(())
}
fn hook_execute_script(
&self,
script: &str,
args: &[&str],
) -> Result<ScriptOutput, crate::Error> {
// Only allow scripts from a trusted, fixed directory to prevent
// arbitrary path execution from untrusted input.
let name = std::path::Path::new(script);
if name.is_absolute()
|| name.components().any(|c| {
matches!(
c,
std::path::Component::ParentDir | std::path::Component::RootDir
)
})
{
return Err(crate::Error::Other(format!(
"script path must be a simple relative name, got: {script}"
)));
}
let repo = self.repo()?;
let hooks_dir = repo.path().join("hooks");
if !hooks_dir.is_dir() {
return Err(crate::Error::Other("hooks directory missing".to_string()));
}
let script_path = hooks_dir.join(name);
let output = std::process::Command::new(&script_path)
.args(args)
.output()
.map_err(|e| crate::Error::Io(e.to_string()))?;
Ok(ScriptOutput {
exit_code: output.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
})
}
fn hook_supported(&self, kind: HookEventKind) -> bool {
matches!(
kind,
HookEventKind::PreReceive
| HookEventKind::PostReceive
| HookEventKind::Update
| HookEventKind::Push
| HookEventKind::Tag
| HookEventKind::MergeRequest
)
}
}
/// Helper to build a `HookRef` change record.
#[allow(dead_code)]
pub fn make_hook_ref(name: &str, old_oid: Option<&str>, new_oid: Option<&str>) -> HookRef {
let old_oid = old_oid.and_then(|s| gix::hash::ObjectId::from_hex(s.as_bytes()).ok());
let new_oid = new_oid.and_then(|s| gix::hash::ObjectId::from_hex(s.as_bytes()).ok());
HookRef {
name: name.to_string(),
old_oid,
new_oid,
}
}