241 lines
8.1 KiB
Rust
241 lines
8.1 KiB
Rust
//! Copyright (c) 2022-2026 GitDataAi All rights reserved.
|
|
|
|
//! Hook manager for GitKS.
|
|
//!
|
|
//! Manages the installation, listing, and removal of git hooks
|
|
//! for bare repositories. Supports server hooks, custom hooks,
|
|
//! and gRPC callback hooks.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::time::Duration;
|
|
|
|
use crate::error::{GitError, GitResult};
|
|
use crate::hooks::runner::{HookResult, generate_hook_runner_script, run_hook_dir};
|
|
use crate::hooks::sanitize::{validate_hook_content, validate_hook_name};
|
|
|
|
/// Manages git hooks across repositories.
|
|
#[derive(Debug, Clone)]
|
|
pub struct HookManager {
|
|
repo_prefix: PathBuf,
|
|
server_hooks_dir: Option<PathBuf>,
|
|
hook_callback_addr: Option<String>,
|
|
hook_timeout: Duration,
|
|
allow_custom_hooks: bool,
|
|
}
|
|
|
|
impl HookManager {
|
|
pub fn new(
|
|
repo_prefix: PathBuf,
|
|
server_hooks_dir: Option<PathBuf>,
|
|
hook_callback_addr: Option<String>,
|
|
hook_timeout: Duration,
|
|
allow_custom_hooks: bool,
|
|
) -> Self {
|
|
Self {
|
|
repo_prefix,
|
|
server_hooks_dir,
|
|
hook_callback_addr,
|
|
hook_timeout,
|
|
allow_custom_hooks,
|
|
}
|
|
}
|
|
|
|
/// Install gitks hook runner scripts into a repository's hooks directory.
|
|
/// Called during repository initialization.
|
|
pub fn install_hooks(&self, repo_path: &Path) -> GitResult<()> {
|
|
let hooks_dir = repo_path.join("hooks");
|
|
std::fs::create_dir_all(&hooks_dir).map_err(GitError::Io)?;
|
|
|
|
let relative_path = repo_path
|
|
.strip_prefix(&self.repo_prefix)
|
|
.unwrap_or(repo_path)
|
|
.to_string_lossy()
|
|
.trim_start_matches('/')
|
|
.to_string();
|
|
|
|
let server_hooks_dir_str = self
|
|
.server_hooks_dir
|
|
.as_ref()
|
|
.map(|p| p.to_string_lossy().into_owned());
|
|
let callback_addr_str = self.hook_callback_addr.clone();
|
|
|
|
for hook_type in &["pre-receive", "update", "post-receive"] {
|
|
let script_content = generate_hook_runner_script(
|
|
hook_type,
|
|
&relative_path,
|
|
server_hooks_dir_str.as_deref(),
|
|
callback_addr_str.as_deref(),
|
|
self.hook_timeout.as_secs(),
|
|
);
|
|
let hook_path = hooks_dir.join(hook_type);
|
|
std::fs::write(&hook_path, script_content).map_err(GitError::Io)?;
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let mut perms = std::fs::metadata(&hook_path)
|
|
.map_err(GitError::Io)?
|
|
.permissions();
|
|
perms.set_mode(0o755);
|
|
std::fs::set_permissions(&hook_path, perms).map_err(GitError::Io)?;
|
|
}
|
|
tracing::debug!(
|
|
hook_type = %hook_type,
|
|
path = %hook_path.display(),
|
|
"installed gitks hook runner"
|
|
);
|
|
}
|
|
|
|
tracing::info!(
|
|
path = %repo_path.display(),
|
|
"hooks installed for repository"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Set a custom hook script for a repository.
|
|
pub fn set_custom_hook(
|
|
&self,
|
|
repo_path: &Path,
|
|
hook_name: &str,
|
|
content: &str,
|
|
) -> GitResult<()> {
|
|
validate_hook_name(hook_name)?;
|
|
if !self.allow_custom_hooks {
|
|
return Err(GitError::PermissionDenied(
|
|
"custom hooks are not allowed on this server".into(),
|
|
));
|
|
}
|
|
validate_hook_content(content)?;
|
|
|
|
let custom_hooks_dir = repo_path.join("custom_hooks").join(hook_name).join("d");
|
|
std::fs::create_dir_all(&custom_hooks_dir).map_err(GitError::Io)?;
|
|
|
|
let script_name = format!("gitks_custom_{}", hook_name.replace('-', "_"));
|
|
let script_path = custom_hooks_dir.join(&script_name);
|
|
std::fs::write(&script_path, content).map_err(GitError::Io)?;
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let mut perms = std::fs::metadata(&script_path)
|
|
.map_err(GitError::Io)?
|
|
.permissions();
|
|
perms.set_mode(0o755);
|
|
std::fs::set_permissions(&script_path, perms).map_err(GitError::Io)?;
|
|
}
|
|
|
|
tracing::info!(
|
|
repo = %repo_path.display(),
|
|
hook_name = %hook_name,
|
|
"custom hook set"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Remove a custom hook from a repository.
|
|
pub fn remove_custom_hook(&self, repo_path: &Path, hook_name: &str) -> GitResult<()> {
|
|
validate_hook_name(hook_name)?;
|
|
let custom_hooks_dir = repo_path.join("custom_hooks").join(hook_name).join("d");
|
|
if custom_hooks_dir.exists() {
|
|
std::fs::remove_dir_all(&custom_hooks_dir).map_err(GitError::Io)?;
|
|
tracing::info!(
|
|
repo = %repo_path.display(),
|
|
hook_name = %hook_name,
|
|
"custom hook removed"
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// List all hooks for a repository.
|
|
pub fn list_hooks(&self, repo_path: &Path) -> GitResult<Vec<HookInfo>> {
|
|
let mut hooks = Vec::new();
|
|
|
|
let hooks_dir = repo_path.join("hooks");
|
|
if hooks_dir.exists() {
|
|
for hook_type in &["pre-receive", "update", "post-receive"] {
|
|
let hook_path = hooks_dir.join(hook_type);
|
|
if hook_path.exists() {
|
|
hooks.push(HookInfo {
|
|
hook_type: hook_type.to_string(),
|
|
level: HookLevel::Server,
|
|
path: hook_path.to_string_lossy().into_owned(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if self.allow_custom_hooks {
|
|
let custom_dir = repo_path.join("custom_hooks");
|
|
if custom_dir.exists() {
|
|
for entry in std::fs::read_dir(&custom_dir).map_err(GitError::Io)? {
|
|
let entry = entry.map_err(GitError::Io)?;
|
|
let name = entry.file_name().to_string_lossy().into_owned();
|
|
if validate_hook_name(&name).is_ok() {
|
|
let path = entry.path();
|
|
let d_dir = path.join("d");
|
|
if d_dir.exists() {
|
|
let script_count =
|
|
std::fs::read_dir(&d_dir).map_err(GitError::Io)?.count();
|
|
if script_count > 0 {
|
|
hooks.push(HookInfo {
|
|
hook_type: name,
|
|
level: HookLevel::Custom,
|
|
path: d_dir.to_string_lossy().into_owned(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(hooks)
|
|
}
|
|
|
|
/// Execute a hook by running server hooks then custom hooks.
|
|
pub fn execute_hook(&self, repo_path: &Path, hook_type: &str, stdin_data: &[u8]) -> HookResult {
|
|
if let Some(ref server_dir) = self.server_hooks_dir {
|
|
let dir = server_dir.join(hook_type).join("d");
|
|
let result = run_hook_dir(&dir, hook_type, stdin_data, self.hook_timeout);
|
|
if !result.accepted {
|
|
return result;
|
|
}
|
|
}
|
|
|
|
if self.allow_custom_hooks {
|
|
let custom_dir = repo_path.join("custom_hooks").join(hook_type).join("d");
|
|
let result = run_hook_dir(&custom_dir, hook_type, stdin_data, self.hook_timeout);
|
|
if !result.accepted {
|
|
return result;
|
|
}
|
|
}
|
|
|
|
HookResult::accepted()
|
|
}
|
|
}
|
|
|
|
/// Information about a hook.
|
|
#[derive(Debug, Clone)]
|
|
pub struct HookInfo {
|
|
pub hook_type: String,
|
|
pub level: HookLevel,
|
|
pub path: String,
|
|
}
|
|
|
|
/// Hook level (server vs custom).
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum HookLevel {
|
|
Server,
|
|
Custom,
|
|
}
|
|
|
|
impl std::fmt::Display for HookLevel {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
HookLevel::Server => write!(f, "server"),
|
|
HookLevel::Custom => write!(f, "custom"),
|
|
}
|
|
}
|
|
}
|