1use crate::config::ConfigSet;
7use crate::repo::Repository;
8use std::fs;
9use std::os::unix::fs::PermissionsExt;
10use std::path::{Path, PathBuf};
11use std::process::{Command, Stdio};
12
13#[cfg(unix)]
14const ENOEXEC: i32 = 8;
15
16#[cfg(unix)]
17fn is_enoexec(err: &std::io::Error) -> bool {
18 err.raw_os_error() == Some(ENOEXEC)
19}
20
21fn stdio_piped(piped: bool) -> Stdio {
22 if piped {
23 Stdio::piped()
24 } else {
25 Stdio::inherit()
26 }
27}
28
29fn spawn_hook_child(
32 hook_path: &Path,
33 hook_args: &[&str],
34 cwd: &Path,
35 git_dir: &Path,
36 extra_env: &[(&str, &str)],
37 stdin_piped: bool,
38 stdout_piped: bool,
39 stderr_piped: bool,
40 use_shell: bool,
41) -> std::io::Result<std::process::Child> {
42 let mut cmd = if use_shell {
43 let mut sh = Command::new("/bin/sh");
44 sh.arg(hook_path);
45 sh
46 } else {
47 Command::new(hook_path)
48 };
49 cmd.args(hook_args)
50 .current_dir(cwd)
51 .env("GIT_DIR", git_dir)
52 .stdin(stdio_piped(stdin_piped))
53 .stdout(stdio_piped(stdout_piped))
54 .stderr(stdio_piped(stderr_piped));
55 for (k, v) in extra_env {
56 cmd.env(k, v);
57 }
58 match cmd.spawn() {
59 Ok(c) => Ok(c),
60 Err(e) => {
61 #[cfg(unix)]
62 {
63 if !use_shell && is_enoexec(&e) {
64 return spawn_hook_child(
65 hook_path,
66 hook_args,
67 cwd,
68 git_dir,
69 extra_env,
70 stdin_piped,
71 stdout_piped,
72 stderr_piped,
73 true,
74 );
75 }
76 }
77 Err(e)
78 }
79 }
80}
81
82#[derive(Debug)]
84pub enum HookResult {
85 Success,
87 NotFound,
89 Failed(i32),
91}
92
93impl HookResult {
94 pub fn is_ok(&self) -> bool {
96 matches!(self, HookResult::Success | HookResult::NotFound)
97 }
98
99 pub fn was_executed(&self) -> bool {
101 matches!(self, HookResult::Success | HookResult::Failed(_))
102 }
103}
104
105pub fn resolve_hooks_dir(repo: &Repository) -> PathBuf {
107 let config = ConfigSet::load(Some(&repo.git_dir), true).ok();
108
109 if let Some(ref config) = config {
110 if let Some(hooks_path) = config.get("core.hooksPath") {
111 let expanded = crate::config::parse_path(&hooks_path);
112 let p = PathBuf::from(expanded);
113 if p.is_absolute() {
114 return p;
115 }
116 if let Ok(cwd) = std::env::current_dir() {
118 return cwd.join(p);
119 }
120 }
121 }
122
123 repo.git_dir.join("hooks")
124}
125
126fn hook_command_path(repo: &Repository, hooks_dir: &Path, hook_name: &str, cwd: &Path) -> PathBuf {
127 let default_hooks_dir = repo.git_dir.join("hooks");
128 if hooks_dir == default_hooks_dir {
129 if cwd == repo.git_dir {
130 return PathBuf::from("hooks").join(hook_name);
131 }
132 if let Some(work_tree) = repo.work_tree.as_deref() {
133 if cwd == work_tree {
134 return PathBuf::from(".git").join("hooks").join(hook_name);
135 }
136 }
137 }
138 hooks_dir.join(hook_name)
139}
140
141pub fn run_hook(
148 repo: &Repository,
149 hook_name: &str,
150 args: &[&str],
151 stdin_data: Option<&[u8]>,
152) -> HookResult {
153 let hooks_dir = resolve_hooks_dir(repo);
154 let hook_path = hooks_dir.join(hook_name);
155
156 if !hook_path.exists() {
158 return HookResult::NotFound;
159 }
160
161 let meta = match fs::metadata(&hook_path) {
163 Ok(m) => m,
164 Err(_) => return HookResult::NotFound,
165 };
166 if meta.permissions().mode() & 0o111 == 0 {
167 let config = ConfigSet::load(Some(&repo.git_dir), true).ok();
169 let show_warning = config
170 .as_ref()
171 .and_then(|c| c.get("advice.ignoredHook"))
172 .map(|v| !matches!(v.to_lowercase().as_str(), "false" | "no" | "off" | "0"))
173 .unwrap_or(true);
174 if show_warning {
175 eprintln!(
176 "hint: The '{}' hook was ignored because it's not set as executable.",
177 hook_name
178 );
179 eprintln!(
180 "hint: You can disable this warning with `git config set advice.ignoredHook false`."
181 );
182 }
183 return HookResult::NotFound;
184 }
185
186 let work_dir = repo.work_tree.as_deref().unwrap_or(&repo.git_dir);
187 let command_path = hook_command_path(repo, &hooks_dir, hook_name, work_dir);
188
189 let stdin_piped = stdin_data.is_some();
190
191 let mut child = match spawn_hook_child(
192 &command_path,
193 args,
194 work_dir,
195 &repo.git_dir,
196 &[],
197 stdin_piped,
198 false,
199 false,
200 false,
201 ) {
202 Ok(c) => c,
203 Err(_) => return HookResult::Failed(1),
204 };
205
206 if let Some(data) = stdin_data {
207 if let Some(ref mut stdin) = child.stdin {
208 use std::io::Write;
209 let _ = stdin.write_all(data);
210 }
211 drop(child.stdin.take());
213 }
214
215 match child.wait() {
216 Ok(status) => {
217 if status.success() {
218 HookResult::Success
219 } else {
220 HookResult::Failed(status.code().unwrap_or(1))
221 }
222 }
223 Err(_) => HookResult::Failed(1),
224 }
225}
226
227pub fn run_hook_in_git_dir(
230 repo: &Repository,
231 hook_name: &str,
232 args: &[&str],
233 stdin_data: Option<&[u8]>,
234 env_vars: &[(&str, &str)],
235) -> (HookResult, Vec<u8>) {
236 let hooks_dir = resolve_hooks_dir(repo);
237 let hook_path = hooks_dir.join(hook_name);
238
239 if !hook_path.exists() {
240 return (HookResult::NotFound, Vec::new());
241 }
242
243 let meta = match fs::metadata(&hook_path) {
244 Ok(m) => m,
245 Err(_) => return (HookResult::NotFound, Vec::new()),
246 };
247 if meta.permissions().mode() & 0o111 == 0 {
248 return (HookResult::NotFound, Vec::new());
249 }
250
251 let command_path = hook_command_path(repo, &hooks_dir, hook_name, &repo.git_dir);
252 let stdin_piped = stdin_data.is_some();
253
254 let mut child = match spawn_hook_child(
255 &command_path,
256 args,
257 &repo.git_dir,
258 &repo.git_dir,
259 env_vars,
260 stdin_piped,
261 true,
262 true,
263 false,
264 ) {
265 Ok(c) => c,
266 Err(_) => return (HookResult::Failed(1), Vec::new()),
267 };
268
269 if let Some(data) = stdin_data {
270 if let Some(ref mut stdin) = child.stdin {
271 use std::io::Write;
272 let _ = stdin.write_all(data);
273 }
274 drop(child.stdin.take());
275 }
276
277 match child.wait_with_output() {
278 Ok(output) => {
279 let mut combined = output.stdout;
280 combined.extend_from_slice(&output.stderr);
281 let result = if output.status.success() {
282 HookResult::Success
283 } else {
284 HookResult::Failed(output.status.code().unwrap_or(1))
285 };
286 (result, combined)
287 }
288 Err(_) => (HookResult::Failed(1), Vec::new()),
289 }
290}
291
292pub fn run_hook_with_env(
294 repo: &Repository,
295 hook_name: &str,
296 args: &[&str],
297 stdin_data: Option<&[u8]>,
298 env_vars: &[(&str, &str)],
299) -> (HookResult, Vec<u8>) {
300 let hooks_dir = resolve_hooks_dir(repo);
301 let hook_path = hooks_dir.join(hook_name);
302
303 if !hook_path.exists() {
304 return (HookResult::NotFound, Vec::new());
305 }
306
307 let meta = match fs::metadata(&hook_path) {
308 Ok(m) => m,
309 Err(_) => return (HookResult::NotFound, Vec::new()),
310 };
311 if meta.permissions().mode() & 0o111 == 0 {
312 return (HookResult::NotFound, Vec::new());
313 }
314
315 let work_dir = repo.work_tree.as_deref().unwrap_or(&repo.git_dir);
316 let command_path = hook_command_path(repo, &hooks_dir, hook_name, work_dir);
317
318 let stdin_piped = stdin_data.is_some();
319
320 let mut child = match spawn_hook_child(
321 &command_path,
322 args,
323 work_dir,
324 &repo.git_dir,
325 env_vars,
326 stdin_piped,
327 true,
328 true,
329 false,
330 ) {
331 Ok(c) => c,
332 Err(_) => return (HookResult::Failed(1), Vec::new()),
333 };
334
335 if let Some(data) = stdin_data {
336 if let Some(ref mut stdin) = child.stdin {
337 use std::io::Write;
338 let _ = stdin.write_all(data);
339 }
340 drop(child.stdin.take());
341 }
342
343 match child.wait_with_output() {
344 Ok(output) => {
345 let mut combined = output.stdout;
346 combined.extend_from_slice(&output.stderr);
347 let result = if output.status.success() {
348 HookResult::Success
349 } else {
350 HookResult::Failed(output.status.code().unwrap_or(1))
351 };
352 (result, combined)
353 }
354 Err(_) => (HookResult::Failed(1), Vec::new()),
355 }
356}
357
358pub fn run_hook_capture(
359 repo: &Repository,
360 hook_name: &str,
361 args: &[&str],
362 stdin_data: Option<&[u8]>,
363) -> (HookResult, Vec<u8>) {
364 let hooks_dir = resolve_hooks_dir(repo);
365 let hook_path = hooks_dir.join(hook_name);
366
367 if !hook_path.exists() {
368 return (HookResult::NotFound, Vec::new());
369 }
370
371 let meta = match fs::metadata(&hook_path) {
372 Ok(m) => m,
373 Err(_) => return (HookResult::NotFound, Vec::new()),
374 };
375 if meta.permissions().mode() & 0o111 == 0 {
376 return (HookResult::NotFound, Vec::new());
377 }
378
379 let work_dir = repo.work_tree.as_deref().unwrap_or(&repo.git_dir);
380 let command_path = hook_command_path(repo, &hooks_dir, hook_name, work_dir);
381
382 let stdin_piped = stdin_data.is_some();
383
384 let mut child = match spawn_hook_child(
385 &command_path,
386 args,
387 work_dir,
388 &repo.git_dir,
389 &[],
390 stdin_piped,
391 true,
392 true,
393 false,
394 ) {
395 Ok(c) => c,
396 Err(_) => return (HookResult::Failed(1), Vec::new()),
397 };
398
399 if let Some(data) = stdin_data {
400 if let Some(ref mut stdin) = child.stdin {
401 use std::io::Write;
402 let _ = stdin.write_all(data);
403 }
404 drop(child.stdin.take());
405 }
406
407 match child.wait_with_output() {
408 Ok(output) => {
409 let mut combined = output.stdout;
410 combined.extend_from_slice(&output.stderr);
411 let result = if output.status.success() {
412 HookResult::Success
413 } else {
414 HookResult::Failed(output.status.code().unwrap_or(1))
415 };
416 (result, combined)
417 }
418 Err(_) => (HookResult::Failed(1), Vec::new()),
419 }
420}