use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use crate::error::{GwmError, Result};
use super::types::HookContext;
const DEFERRED_HOOKS_VERSION: u8 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeferredHooks {
pub version: u8,
pub worktree_path: PathBuf,
pub branch_name: String,
pub repo_root: PathBuf,
pub repo_name: String,
pub commands: Vec<String>,
pub trust_verified: bool,
}
impl DeferredHooks {
pub fn new(context: &HookContext, commands: Vec<String>, trust_verified: bool) -> Self {
Self {
version: DEFERRED_HOOKS_VERSION,
worktree_path: context.worktree_path.clone(),
branch_name: context.branch_name.clone(),
repo_root: context.repo_root.clone(),
repo_name: context.repo_name.clone(),
commands,
trust_verified,
}
}
pub fn to_hook_context(&self) -> HookContext {
HookContext {
worktree_path: self.worktree_path.clone(),
branch_name: self.branch_name.clone(),
repo_root: self.repo_root.clone(),
repo_name: self.repo_name.clone(),
}
}
pub fn write_to_file(&self, path: &Path) -> Result<()> {
let json = serde_json::to_string_pretty(self)
.map_err(|e| GwmError::Config(format!("Failed to serialize deferred hooks: {}", e)))?;
fs::write(path, json)
.map_err(|e| GwmError::Config(format!("Failed to write deferred hooks file: {}", e)))?;
Ok(())
}
pub fn read_from_file(path: &Path) -> Result<Self> {
let content = fs::read_to_string(path)
.map_err(|e| GwmError::Config(format!("Failed to read deferred hooks file: {}", e)))?;
let hooks: DeferredHooks = serde_json::from_str(&content)
.map_err(|e| GwmError::Config(format!("Failed to parse deferred hooks file: {}", e)))?;
if hooks.version != DEFERRED_HOOKS_VERSION {
return Err(GwmError::Config(format!(
"Unsupported deferred hooks version: {} (expected {})",
hooks.version, DEFERRED_HOOKS_VERSION
)));
}
Ok(hooks)
}
pub fn delete_file(path: &Path) -> Result<()> {
if path.exists() {
fs::remove_file(path).map_err(|e| {
GwmError::Config(format!("Failed to delete deferred hooks file: {}", e))
})?;
}
Ok(())
}
}
pub fn hooks_file_path() -> Option<PathBuf> {
std::env::var_os("GWM_HOOKS_FILE").map(PathBuf::from)
}
pub fn try_write_deferred_hooks(
context: &HookContext,
commands: Vec<String>,
trust_verified: bool,
) -> Result<bool> {
let Some(file_path) = hooks_file_path() else {
return Ok(false);
};
if commands.is_empty() {
return Ok(false);
}
let deferred = DeferredHooks::new(context, commands, trust_verified);
deferred.write_to_file(&file_path)?;
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_deferred_hooks_serialization() {
let context = HookContext {
worktree_path: PathBuf::from("/path/to/worktree"),
branch_name: "feature/test".to_string(),
repo_root: PathBuf::from("/path/to/repo"),
repo_name: "test-repo".to_string(),
};
let hooks = DeferredHooks::new(
&context,
vec!["npm install".to_string(), "npm run build".to_string()],
true,
);
let json = serde_json::to_string(&hooks).unwrap();
let parsed: DeferredHooks = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.version, DEFERRED_HOOKS_VERSION);
assert_eq!(parsed.worktree_path, context.worktree_path);
assert_eq!(parsed.branch_name, context.branch_name);
assert_eq!(parsed.commands.len(), 2);
assert!(parsed.trust_verified);
}
#[test]
fn test_write_and_read_file() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("hooks.json");
let context = HookContext {
worktree_path: PathBuf::from("/path/to/worktree"),
branch_name: "feature/test".to_string(),
repo_root: PathBuf::from("/path/to/repo"),
repo_name: "test-repo".to_string(),
};
let hooks = DeferredHooks::new(&context, vec!["npm install".to_string()], true);
hooks.write_to_file(&file_path).unwrap();
let loaded = DeferredHooks::read_from_file(&file_path).unwrap();
assert_eq!(loaded.branch_name, "feature/test");
assert_eq!(loaded.commands, vec!["npm install"]);
assert!(loaded.trust_verified);
}
#[test]
fn test_to_hook_context() {
let context = HookContext {
worktree_path: PathBuf::from("/path/to/worktree"),
branch_name: "feature/test".to_string(),
repo_root: PathBuf::from("/path/to/repo"),
repo_name: "test-repo".to_string(),
};
let hooks = DeferredHooks::new(&context, vec![], true);
let converted = hooks.to_hook_context();
assert_eq!(converted.worktree_path, context.worktree_path);
assert_eq!(converted.branch_name, context.branch_name);
assert_eq!(converted.repo_root, context.repo_root);
assert_eq!(converted.repo_name, context.repo_name);
}
#[test]
fn test_read_from_file_version_mismatch() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("hooks.json");
let content = r#"{
"version": 99,
"worktree_path": "/path/to/worktree",
"branch_name": "test",
"repo_root": "/path/to/repo",
"repo_name": "repo",
"commands": [],
"trust_verified": true
}"#;
std::fs::write(&file_path, content).unwrap();
let result = DeferredHooks::read_from_file(&file_path);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("Unsupported deferred hooks version"),
"Error message should mention version mismatch: {}",
err
);
}
#[test]
fn test_read_from_file_malformed_json() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("hooks.json");
std::fs::write(&file_path, "{ invalid json }").unwrap();
let result = DeferredHooks::read_from_file(&file_path);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("Failed to parse"),
"Error message should mention parse failure: {}",
err
);
}
#[test]
fn test_trust_verified_false_preserved() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("hooks.json");
let context = HookContext {
worktree_path: PathBuf::from("/path/to/worktree"),
branch_name: "feature/test".to_string(),
repo_root: PathBuf::from("/path/to/repo"),
repo_name: "test-repo".to_string(),
};
let hooks = DeferredHooks::new(&context, vec!["npm install".to_string()], false);
assert!(!hooks.trust_verified);
hooks.write_to_file(&file_path).unwrap();
let loaded = DeferredHooks::read_from_file(&file_path).unwrap();
assert!(
!loaded.trust_verified,
"trust_verified: false must be preserved after serialization"
);
}
#[test]
fn test_delete_file_not_exist() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("nonexistent.json");
let result = DeferredHooks::delete_file(&file_path);
assert!(result.is_ok());
}
#[test]
fn test_delete_file_exists() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("hooks.json");
std::fs::write(&file_path, "{}").unwrap();
assert!(file_path.exists());
let result = DeferredHooks::delete_file(&file_path);
assert!(result.is_ok());
assert!(!file_path.exists());
}
#[test]
fn test_deferred_hooks_empty_commands() {
let context = HookContext {
worktree_path: PathBuf::from("/path/to/worktree"),
branch_name: "feature/test".to_string(),
repo_root: PathBuf::from("/path/to/repo"),
repo_name: "test-repo".to_string(),
};
let hooks = DeferredHooks::new(&context, vec![], true);
assert!(hooks.commands.is_empty());
assert!(hooks.trust_verified);
let json = serde_json::to_string(&hooks).unwrap();
let parsed: DeferredHooks = serde_json::from_str(&json).unwrap();
assert!(parsed.commands.is_empty());
}
#[test]
fn test_deferred_hooks_version_constant() {
assert_eq!(DEFERRED_HOOKS_VERSION, 1);
}
#[test]
fn test_read_from_file_not_exist() {
let result = DeferredHooks::read_from_file(Path::new("/nonexistent/hooks.json"));
assert!(result.is_err());
}
}