1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
//! Hook execution utilities.
//!
//! Provides a reusable function for running Git hooks from `.git/hooks/`
//! or from the directory configured via `core.hooksPath`.
use crate::config::ConfigSet;
use crate::repo::Repository;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::process::Command;
/// Result of running a hook.
#[derive(Debug)]
pub enum HookResult {
/// Hook ran successfully (exit code 0).
Success,
/// Hook does not exist or is not executable — treated as success.
NotFound,
/// Hook ran but returned a non-zero exit code.
Failed(i32),
}
impl HookResult {
/// Returns true if the hook was successful or not found.
pub fn is_ok(&self) -> bool {
matches!(self, HookResult::Success | HookResult::NotFound)
}
/// Returns true if the hook existed and ran (regardless of exit code).
pub fn was_executed(&self) -> bool {
matches!(self, HookResult::Success | HookResult::Failed(_))
}
}
/// Resolve the hooks directory from config or fall back to `$GIT_DIR/hooks`.
pub fn resolve_hooks_dir(repo: &Repository) -> PathBuf {
let config = ConfigSet::load(Some(&repo.git_dir), true).ok();
if let Some(ref config) = config {
if let Some(hooks_path) = config.get("core.hooksPath") {
let expanded = crate::config::parse_path(&hooks_path);
let p = PathBuf::from(expanded);
if p.is_absolute() {
return p;
}
// Relative to the working directory (git behaviour).
if let Ok(cwd) = std::env::current_dir() {
return cwd.join(p);
}
}
}
repo.git_dir.join("hooks")
}
/// Run a hook by name with the given arguments.
///
/// The hook is looked up in the hooks directory (respecting `core.hooksPath`).
/// If the hook file doesn't exist or isn't executable, returns `HookResult::NotFound`.
///
/// `stdin_data` can optionally provide data to write to the hook's stdin.
pub fn run_hook(
repo: &Repository,
hook_name: &str,
args: &[&str],
stdin_data: Option<&[u8]>,
) -> HookResult {
let hooks_dir = resolve_hooks_dir(repo);
let hook_path = hooks_dir.join(hook_name);
// If the hook doesn't exist, silently succeed (git behaviour).
if !hook_path.exists() {
return HookResult::NotFound;
}
// Check if executable.
let meta = match fs::metadata(&hook_path) {
Ok(m) => m,
Err(_) => return HookResult::NotFound,
};
if meta.permissions().mode() & 0o111 == 0 {
return HookResult::NotFound;
}
let work_dir = repo
.work_tree
.as_deref()
.unwrap_or(&repo.git_dir);
let mut cmd = Command::new(&hook_path);
cmd.args(args)
.current_dir(work_dir)
.env("GIT_DIR", &repo.git_dir);
if stdin_data.is_some() {
cmd.stdin(std::process::Stdio::piped());
}
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(_) => return HookResult::Failed(1),
};
if let Some(data) = stdin_data {
if let Some(ref mut stdin) = child.stdin {
use std::io::Write;
let _ = stdin.write_all(data);
}
// Drop stdin to signal EOF
drop(child.stdin.take());
}
match child.wait() {
Ok(status) => {
if status.success() {
HookResult::Success
} else {
HookResult::Failed(status.code().unwrap_or(1))
}
}
Err(_) => HookResult::Failed(1),
}
}