//! 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 { // 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 { // 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 { // 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, } }