Skip to main content

git_claw/infra/
hook.rs

1//! Lifecycle hook execution and policy enforcement.
2
3use crate::cli::output::print_warning;
4use crate::infra::error::HookError;
5use std::collections::BTreeMap;
6use std::path::Path;
7use std::process::Command;
8
9/// Executes a shell command inside `working_dir` with `env_vars` injected.
10pub fn execute_hook_command(
11    command_str: &str,
12    working_dir: &Path,
13    env_vars: &BTreeMap<String, String>,
14) -> Result<bool, HookError> {
15    #[cfg(unix)]
16    let mut cmd = Command::new("sh");
17    #[cfg(unix)]
18    cmd.args(["-c", command_str]);
19
20    #[cfg(windows)]
21    let mut cmd = Command::new("cmd");
22    #[cfg(windows)]
23    cmd.args(["/C", command_str]);
24
25    cmd.current_dir(working_dir);
26    for (k, v) in env_vars {
27        cmd.env(k, v);
28    }
29
30    let status = cmd
31        .status()
32        .map_err(|e| HookError::ExecutionFailed(format!("Failed to spawn hook command: {}", e)))?;
33
34    Ok(status.success())
35}
36
37/// Runs `post_start` hook: non-fatal warning on failure, does not abort worktree creation.
38pub fn run_post_start_hook(
39    hook: Option<&str>,
40    worktree_path: &Path,
41    env_vars: &BTreeMap<String, String>,
42) -> Result<bool, HookError> {
43    if let Some(cmd) = hook {
44        let success = execute_hook_command(cmd, worktree_path, env_vars)?;
45        if !success {
46            print_warning(format!(
47                "post_start hook '{}' exited with non-zero status; retaining worktree",
48                cmd
49            ));
50        }
51        return Ok(success);
52    }
53    Ok(true)
54}
55
56/// Runs `pre_finish` hook: fatal gatekeeper aborting execution unless `force` is true.
57pub fn run_pre_finish_hook(
58    hook: Option<&str>,
59    worktree_path: &Path,
60    env_vars: &BTreeMap<String, String>,
61    force: bool,
62) -> Result<bool, HookError> {
63    if let Some(cmd) = hook {
64        let success = execute_hook_command(cmd, worktree_path, env_vars)?;
65        if !success {
66            if force {
67                print_warning(format!(
68                    "pre_finish hook '{}' failed, but continuing due to --force",
69                    cmd
70                ));
71                return Ok(false);
72            } else {
73                return Err(HookError::PreFinishFailed {
74                    hook: cmd.to_string(),
75                    code: 1,
76                });
77            }
78        }
79        return Ok(true);
80    }
81    Ok(true)
82}
83
84/// Runs `post_finish` hook: non-fatal warning on failure.
85pub fn run_post_finish_hook(
86    hook: Option<&str>,
87    repo_root: &Path,
88    env_vars: &BTreeMap<String, String>,
89) -> Result<bool, HookError> {
90    if let Some(cmd) = hook {
91        let success = execute_hook_command(cmd, repo_root, env_vars)?;
92        if !success {
93            print_warning(format!(
94                "post_finish hook '{}' exited with non-zero status",
95                cmd
96            ));
97        }
98        return Ok(success);
99    }
100    Ok(true)
101}