use std::collections::HashMap;
use std::process::{Command, Stdio};
use std::time::Instant;
use super::types::{HookContext, HookExecutionDetail, HookResult};
use crate::config::Config;
use crate::error::Result;
fn prepare_hook_env(context: &HookContext) -> HashMap<String, String> {
let mut env: HashMap<String, String> = std::env::vars().collect();
env.insert(
"GWM_WORKTREE_PATH".to_string(),
context.worktree_path.display().to_string(),
);
env.insert("GWM_BRANCH_NAME".to_string(), context.branch_name.clone());
env.insert(
"GWM_REPO_ROOT".to_string(),
context.repo_root.display().to_string(),
);
env.insert("GWM_REPO_NAME".to_string(), context.repo_name.clone());
env
}
fn execute_command(
command: &str,
cwd: &std::path::Path,
env: &HashMap<String, String>,
) -> std::io::Result<i32> {
let status = Command::new("sh")
.args(["-c", command])
.current_dir(cwd)
.envs(env)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()?;
Ok(status.code().unwrap_or(1))
}
pub fn run_post_create_hooks(config: &Config, context: &HookContext) -> Result<HookResult> {
let commands = match config.post_create_commands() {
Some(cmds) if !cmds.is_empty() => cmds,
_ => return Ok(HookResult::no_hooks()),
};
execute_hooks(commands, context)
}
pub fn run_post_create_hooks_with_commands(
commands: &[String],
context: &HookContext,
) -> Result<HookResult> {
if commands.is_empty() {
return Ok(HookResult::no_hooks());
}
execute_hooks(commands, context)
}
fn execute_hooks(commands: &[String], context: &HookContext) -> Result<HookResult> {
let env = prepare_hook_env(context);
let total = commands.len();
let total_start = Instant::now();
let mut details: Vec<HookExecutionDetail> = Vec::with_capacity(total);
println!(
"\n\x1b[36mRunning post_create hooks ({} command{})...\x1b[0m",
total,
if total > 1 { "s" } else { "" }
);
for (i, cmd) in commands.iter().enumerate() {
println!(" [{}/{}] Executing: {}", i + 1, total, cmd);
let cmd_start = Instant::now();
let result = execute_command(cmd, &context.worktree_path, &env);
let cmd_duration = cmd_start.elapsed();
match result {
Ok(0) => {
println!(
" \x1b[32m✓ [{}/{}] {} ({:.1}s)\x1b[0m",
i + 1,
total,
cmd,
cmd_duration.as_secs_f64()
);
details.push(HookExecutionDetail::success(cmd.clone(), cmd_duration));
}
Ok(code) => {
println!(
" \x1b[31m✗ [{}/{}] {} (failed, exit code: {}, {:.1}s)\x1b[0m",
i + 1,
total,
cmd,
code,
cmd_duration.as_secs_f64()
);
details.push(HookExecutionDetail::failure_with_code(
cmd.clone(),
cmd_duration,
code,
));
return Ok(HookResult::failure(
i + 1,
cmd.clone(),
code,
total_start.elapsed(),
details,
));
}
Err(e) => {
let error_msg = e.to_string();
println!(
" \x1b[31m✗ [{}/{}] {} (error: {}, {:.1}s)\x1b[0m",
i + 1,
total,
cmd,
error_msg,
cmd_duration.as_secs_f64()
);
details.push(HookExecutionDetail::failure_with_error(
cmd.clone(),
cmd_duration,
error_msg,
));
return Ok(HookResult::failure(
i + 1,
cmd.clone(),
1,
total_start.elapsed(),
details,
));
}
}
}
let total_duration = total_start.elapsed();
println!(
"\x1b[32m✓ post_create hooks completed ({}/{}, {:.1}s)\x1b[0m",
total,
total,
total_duration.as_secs_f64()
);
Ok(HookResult::success(total, total_duration, details))
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use tempfile::TempDir;
fn create_test_context() -> HookContext {
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(),
}
}
#[test]
fn test_prepare_hook_env() {
let context = create_test_context();
let env = prepare_hook_env(&context);
assert_eq!(env.get("GWM_WORKTREE_PATH").unwrap(), "/path/to/worktree");
assert_eq!(env.get("GWM_BRANCH_NAME").unwrap(), "feature/test");
assert_eq!(env.get("GWM_REPO_ROOT").unwrap(), "/path/to/repo");
assert_eq!(env.get("GWM_REPO_NAME").unwrap(), "test-repo");
}
#[test]
fn test_prepare_hook_env_inherits_system_env() {
std::env::set_var("GWM_TEST_INHERIT", "test_value");
let context = create_test_context();
let env = prepare_hook_env(&context);
assert_eq!(env.get("GWM_TEST_INHERIT").unwrap(), "test_value");
std::env::remove_var("GWM_TEST_INHERIT");
}
#[test]
fn test_run_hooks_no_commands() {
let config = Config::default();
let context = HookContext {
worktree_path: PathBuf::from("/tmp"),
branch_name: "test".to_string(),
repo_root: PathBuf::from("/tmp"),
repo_name: "test".to_string(),
};
let result = run_post_create_hooks(&config, &context).unwrap();
assert!(result.success);
assert_eq!(result.executed_count, 0);
}
#[test]
fn test_run_post_create_hooks_with_commands_empty() {
let temp_dir = TempDir::new().unwrap();
let context = HookContext {
worktree_path: temp_dir.path().to_path_buf(),
branch_name: "test".to_string(),
repo_root: temp_dir.path().to_path_buf(),
repo_name: "test".to_string(),
};
let result = run_post_create_hooks_with_commands(&[], &context).unwrap();
assert!(result.success);
assert_eq!(result.executed_count, 0);
}
#[test]
fn test_run_post_create_hooks_with_commands_success() {
let temp_dir = TempDir::new().unwrap();
let context = HookContext {
worktree_path: temp_dir.path().to_path_buf(),
branch_name: "test".to_string(),
repo_root: temp_dir.path().to_path_buf(),
repo_name: "test".to_string(),
};
let commands = vec!["echo hello".to_string(), "echo world".to_string()];
let result = run_post_create_hooks_with_commands(&commands, &context).unwrap();
assert!(result.success);
assert_eq!(result.executed_count, 2);
assert!(result.failed_command.is_none());
}
#[test]
fn test_run_post_create_hooks_with_commands_failure() {
let temp_dir = TempDir::new().unwrap();
let context = HookContext {
worktree_path: temp_dir.path().to_path_buf(),
branch_name: "test".to_string(),
repo_root: temp_dir.path().to_path_buf(),
repo_name: "test".to_string(),
};
let commands = vec![
"echo first".to_string(),
"exit 1".to_string(), "echo third".to_string(),
];
let result = run_post_create_hooks_with_commands(&commands, &context).unwrap();
assert!(!result.success);
assert_eq!(result.executed_count, 2); assert_eq!(result.failed_command, Some("exit 1".to_string()));
assert_eq!(result.exit_code, Some(1));
}
#[test]
fn test_execute_command_success() {
let temp_dir = TempDir::new().unwrap();
let env: HashMap<String, String> = std::env::vars().collect();
let result = execute_command("echo test", temp_dir.path(), &env);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0);
}
#[test]
fn test_execute_command_failure() {
let temp_dir = TempDir::new().unwrap();
let env: HashMap<String, String> = std::env::vars().collect();
let result = execute_command("exit 42", temp_dir.path(), &env);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_execute_command_with_gwm_env() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("output.txt");
let context = HookContext {
worktree_path: temp_dir.path().to_path_buf(),
branch_name: "feature/test-branch".to_string(),
repo_root: temp_dir.path().to_path_buf(),
repo_name: "test-repo".to_string(),
};
let env = prepare_hook_env(&context);
let cmd = format!("echo $GWM_BRANCH_NAME > {}", test_file.display());
let result = execute_command(&cmd, temp_dir.path(), &env);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0);
let content = std::fs::read_to_string(&test_file).unwrap();
assert_eq!(content.trim(), "feature/test-branch");
}
}