set_git_hooks_dir/
installer.rs

1use std::env;
2use std::fs::File;
3use std::io::{self, BufRead, BufReader};
4use std::path::{Path, PathBuf};
5use std::process::Command;
6
7const CI_ENV_VARS: &[&str] = &[
8    "SET_GIT_HOOKS_DIR_SKIP",
9    "GITHUB_ACTION",
10    "CI",
11    "JENKINS_URL",
12];
13
14fn find_dot_git(hooks_dir: &Path, base_dir: &Path) -> io::Result<PathBuf> {
15    let mut cur = base_dir;
16    loop {
17        let dir = cur.join(hooks_dir);
18        if dir.is_dir() {
19            let dot_git = cur.join(".git");
20            if dot_git.exists() {
21                return Ok(dot_git);
22            }
23        }
24        let Some(parent) = cur.parent() else {
25            let msg = format!(
26                "Directory {hooks_dir:?} is not found at any root of Git repository in {base_dir:?}",
27            );
28            return Err(io::Error::new(io::ErrorKind::Other, msg));
29        };
30        cur = parent;
31    }
32}
33
34/// Setup the Git hooks directory path specified by `dir` argument.
35pub fn setup(hooks_dir: impl AsRef<Path>, base_dir: impl AsRef<Path>) -> io::Result<()> {
36    for var in CI_ENV_VARS {
37        if matches!(env::var(var), Ok(v) if !v.is_empty()) {
38            return Ok(());
39        }
40    }
41
42    let hooks_dir = hooks_dir.as_ref();
43
44    let dot_git = find_dot_git(hooks_dir, base_dir.as_ref())?;
45    if dot_git.is_dir() {
46        let config = File::open(dot_git.join("config"))?;
47        for line in BufReader::new(config).lines() {
48            if line?.starts_with("\thooksPath = ") {
49                return Ok(());
50            }
51        }
52    }
53
54    let git_var = env::var("SET_GIT_HOOKS_DIR_GIT");
55    let git = match &git_var {
56        Ok(var) if !var.is_empty() => var.as_str(),
57        _ => "git",
58    };
59
60    let mut cmd = Command::new(git);
61    cmd.arg("config").arg("core.hooksPath").arg(hooks_dir);
62    if let Some(root) = dot_git.parent() {
63        cmd.current_dir(root);
64    }
65
66    let output = cmd.output()?;
67    if !output.status.success() {
68        let stderr = String::from_utf8_lossy(&output.stderr);
69        return Err(io::Error::new(
70            io::ErrorKind::Other,
71            format!("`{git} config core.hooksPath {hooks_dir:?}` failed: {stderr}"),
72        ));
73    }
74
75    Ok(())
76}