#![allow(dead_code)]
#![allow(unused_variables)]
use crate::{Error, Result};
use std::path::PathBuf;
use std::fs;
use std::os::unix::fs::PermissionsExt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HookType {
PostCheckout,
PreCommit,
PostMerge,
}
impl HookType {
pub fn filename(&self) -> &'static str {
match self {
HookType::PostCheckout => "post-checkout",
HookType::PreCommit => "pre-commit",
HookType::PostMerge => "post-merge",
}
}
pub fn all() -> &'static [HookType] {
&[HookType::PostCheckout, HookType::PreCommit, HookType::PostMerge]
}
}
#[derive(Debug, Clone)]
pub struct HookStatus {
pub hook_type: HookType,
pub installed: bool,
pub path: PathBuf,
pub is_helios_hook: bool,
}
#[derive(Debug, Clone)]
pub struct HookConfig {
pub database: String,
pub migration_dir: Option<String>,
pub verbose: bool,
}
impl Default for HookConfig {
fn default() -> Self {
Self {
database: String::new(),
migration_dir: None,
verbose: false,
}
}
}
pub struct HookManager {
repo_path: PathBuf,
config: HookConfig,
}
impl HookManager {
pub fn new(repo_path: PathBuf, config: HookConfig) -> Self {
Self { repo_path, config }
}
fn hooks_dir(&self) -> PathBuf {
self.repo_path.join(".git").join("hooks")
}
fn hook_path(&self, hook_type: HookType) -> PathBuf {
self.hooks_dir().join(hook_type.filename())
}
fn generate_post_checkout(&self) -> String {
let db_arg = if self.config.database.is_empty() {
String::new()
} else {
format!("--database \"{}\"", self.config.database)
};
format!(r#"#!/bin/sh
# HeliosDB-Nano Git Hook: post-checkout
# Auto-switch database branch when Git branch changes
#
# Arguments:
# $1 - ref of previous HEAD
# $2 - ref of new HEAD
# $3 - flag: 1 = branch checkout, 0 = file checkout
PREV_HEAD="$1"
NEW_HEAD="$2"
CHECKOUT_TYPE="$3"
# Only run on branch checkouts, not file checkouts
if [ "$CHECKOUT_TYPE" != "1" ]; then
exit 0
fi
# Get current Git branch
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ -z "$GIT_BRANCH" ]; then
exit 0
fi
# Sync database with Git branch
if command -v helios >/dev/null 2>&1; then
helios git sync {db_arg} 2>/dev/null || true
{verbose}
fi
"#,
db_arg = db_arg,
verbose = if self.config.verbose {
"echo \"[HeliosDB] Synced to branch: $GIT_BRANCH\""
} else {
""
}
)
}
fn generate_pre_commit(&self) -> String {
let db_arg = if self.config.database.is_empty() {
String::new()
} else {
format!("--database \"{}\"", self.config.database)
};
let migration_check = self.config.migration_dir.as_ref().map(|dir| {
format!(r#"
# Validate migrations
if [ -d "{dir}" ]; then
helios migration validate --dir "{dir}" {db_arg}
if [ $? -ne 0 ]; then
echo "[HeliosDB] Migration validation failed"
exit 1
fi
fi
"#,
dir = dir,
db_arg = db_arg
)
}).unwrap_or_default();
format!(r#"#!/bin/sh
# HeliosDB-Nano Git Hook: pre-commit
# Validate schema and migrations before commit
{migration_check}
# Validate schema consistency
if command -v helios >/dev/null 2>&1; then
helios schema validate {db_arg} 2>/dev/null
if [ $? -ne 0 ]; then
echo "[HeliosDB] Schema validation failed"
exit 1
fi
fi
exit 0
"#,
migration_check = migration_check,
db_arg = db_arg
)
}
fn generate_post_merge(&self) -> String {
let db_arg = if self.config.database.is_empty() {
String::new()
} else {
format!("--database \"{}\"", self.config.database)
};
format!(r#"#!/bin/sh
# HeliosDB-Nano Git Hook: post-merge
# Sync database state after merge
# Get current Git branch
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ -z "$GIT_BRANCH" ]; then
exit 0
fi
if command -v helios >/dev/null 2>&1; then
# Apply any pending migrations
helios migration apply {db_arg} --auto 2>/dev/null || true
# Sync database state
helios git sync {db_arg} 2>/dev/null || true
{verbose}
fi
exit 0
"#,
db_arg = db_arg,
verbose = if self.config.verbose {
"echo \"[HeliosDB] Synced after merge on branch: $GIT_BRANCH\""
} else {
""
}
)
}
pub fn generate(&self, hook_type: HookType) -> String {
match hook_type {
HookType::PostCheckout => self.generate_post_checkout(),
HookType::PreCommit => self.generate_pre_commit(),
HookType::PostMerge => self.generate_post_merge(),
}
}
pub fn install(&self, hook_type: HookType) -> Result<()> {
let hooks_dir = self.hooks_dir();
if !hooks_dir.exists() {
fs::create_dir_all(&hooks_dir)
.map_err(|e| Error::io(format!("Failed to create hooks directory: {}", e)))?;
}
let hook_path = self.hook_path(hook_type);
if hook_path.exists() {
let content = fs::read_to_string(&hook_path)
.map_err(|e| Error::io(format!("Failed to read existing hook: {}", e)))?;
if !content.contains("HeliosDB-Nano Git Hook") {
let backup_path = hook_path.with_extension("backup");
fs::rename(&hook_path, &backup_path)
.map_err(|e| Error::io(format!("Failed to backup existing hook: {}", e)))?;
tracing::info!("Backed up existing {} hook to {:?}", hook_type.filename(), backup_path);
}
}
let script = self.generate(hook_type);
fs::write(&hook_path, &script)
.map_err(|e| Error::io(format!("Failed to write hook: {}", e)))?;
#[cfg(unix)]
{
let mut perms = fs::metadata(&hook_path)
.map_err(|e| Error::io(format!("Failed to get hook permissions: {}", e)))?
.permissions();
perms.set_mode(0o755);
fs::set_permissions(&hook_path, perms)
.map_err(|e| Error::io(format!("Failed to set hook permissions: {}", e)))?;
}
tracing::info!("Installed {} hook at {:?}", hook_type.filename(), hook_path);
Ok(())
}
pub fn install_all(&self) -> Result<()> {
for hook_type in HookType::all() {
self.install(*hook_type)?;
}
Ok(())
}
pub fn uninstall(&self, hook_type: HookType) -> Result<()> {
let hook_path = self.hook_path(hook_type);
if hook_path.exists() {
let content = fs::read_to_string(&hook_path)
.map_err(|e| Error::io(format!("Failed to read hook: {}", e)))?;
if content.contains("HeliosDB-Nano Git Hook") {
fs::remove_file(&hook_path)
.map_err(|e| Error::io(format!("Failed to remove hook: {}", e)))?;
let backup_path = hook_path.with_extension("backup");
if backup_path.exists() {
fs::rename(&backup_path, &hook_path)
.map_err(|e| Error::io(format!("Failed to restore backup hook: {}", e)))?;
tracing::info!("Restored backup {} hook", hook_type.filename());
}
tracing::info!("Uninstalled {} hook", hook_type.filename());
} else {
tracing::warn!(
"{} hook exists but is not a HeliosDB hook, skipping",
hook_type.filename()
);
}
}
Ok(())
}
pub fn uninstall_all(&self) -> Result<()> {
for hook_type in HookType::all() {
self.uninstall(*hook_type)?;
}
Ok(())
}
pub fn status(&self) -> Vec<HookStatus> {
HookType::all()
.iter()
.map(|&hook_type| {
let path = self.hook_path(hook_type);
let installed = path.exists();
let is_helios_hook = if installed {
fs::read_to_string(&path)
.map(|c| c.contains("HeliosDB-Nano Git Hook"))
.unwrap_or(false)
} else {
false
};
HookStatus {
hook_type,
installed,
path,
is_helios_hook,
}
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_hook_type_filename() {
assert_eq!(HookType::PostCheckout.filename(), "post-checkout");
assert_eq!(HookType::PreCommit.filename(), "pre-commit");
assert_eq!(HookType::PostMerge.filename(), "post-merge");
}
#[test]
fn test_generate_post_checkout() {
let config = HookConfig {
database: "/path/to/db".to_string(),
verbose: true,
..Default::default()
};
let manager = HookManager::new(PathBuf::from("/tmp"), config);
let script = manager.generate(HookType::PostCheckout);
assert!(script.contains("HeliosDB-Nano Git Hook"));
assert!(script.contains("post-checkout"));
assert!(script.contains("helios git sync"));
}
#[test]
fn test_hook_manager_creation() {
let config = HookConfig::default();
let manager = HookManager::new(PathBuf::from("/tmp/repo"), config);
assert_eq!(manager.hooks_dir(), PathBuf::from("/tmp/repo/.git/hooks"));
}
}