Skip to content

Commit cf8f874

Browse files
committed
feat: Generic hook running
1 parent f1694b3 commit cf8f874

File tree

4 files changed

+122
-0
lines changed

4 files changed

+122
-0
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pre-release-replacements = [
3333
git2 = { version = "0.14", default-features = false }
3434
log = "0.4"
3535
itertools = "0.10"
36+
which = "4"
3637

3738
[dev-dependencies]
3839
git-fixture = { version = "0.2" }

src/hooks.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use std::io::Write;
2+
3+
#[derive(Clone, Debug)]
4+
pub struct Hooks {
5+
root: std::path::PathBuf,
6+
}
7+
8+
impl Hooks {
9+
pub fn new(hook_root: impl Into<std::path::PathBuf>) -> Self {
10+
Self {
11+
root: hook_root.into(),
12+
}
13+
}
14+
15+
pub fn with_repo(repo: &git2::Repository) -> Result<Self, git2::Error> {
16+
let config = repo.config()?;
17+
let root = config
18+
.get_path("core.hooksPath")
19+
.unwrap_or_else(|_| repo.path().join("hooks"));
20+
Ok(Self::new(root))
21+
}
22+
23+
pub fn root(&self) -> &std::path::Path {
24+
&self.root
25+
}
26+
27+
pub fn run_hook(
28+
&self,
29+
repo: &git2::Repository,
30+
name: &str,
31+
args: &[&str],
32+
stdin: Option<&[u8]>,
33+
env: &[(&str, &str)],
34+
) -> Result<i32, std::io::Error> {
35+
let hook_path = self.root().join(name);
36+
if !hook_path.exists() {
37+
return Ok(0);
38+
}
39+
40+
let path = {
41+
let mut path_components: Vec<std::path::PathBuf> =
42+
vec![std::fs::canonicalize(self.root())?];
43+
if let Some(path) = std::env::var_os(std::ffi::OsStr::new("PATH")) {
44+
path_components.extend(std::env::split_paths(&path));
45+
}
46+
std::env::join_paths(path_components)
47+
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?
48+
};
49+
50+
let sh_path = crate::utils::git_sh().ok_or_else(|| {
51+
std::io::Error::new(std::io::ErrorKind::NotFound, "No `sh` for running hooks")
52+
})?;
53+
54+
// From `githooks(5)`:
55+
// > Before Git invokes a hook, it changes its working directory to either $GIT_DIR in a bare
56+
// > repository or the root of the working tree in a non-bare repository. An exception are
57+
// > hooks triggered during a push (pre-receive, update, post-receive, post-update,
58+
// > push-to-checkout) which are always executed in $GIT_DIR.
59+
let cwd = if PUSH_HOOKS.contains(&name) {
60+
repo.path()
61+
} else {
62+
repo.workdir().unwrap_or_else(|| repo.path())
63+
};
64+
65+
let mut cmd = std::process::Command::new(sh_path);
66+
cmd.arg("-c")
67+
.arg(format!("{} \"$@\"", name))
68+
.arg(name) // "$@" expands "$1" "$2" "$3" ... but we also must specify $0.
69+
.args(args)
70+
.env("PATH", path)
71+
.current_dir(cwd)
72+
.stdin(std::process::Stdio::piped());
73+
for (key, value) in env.iter().copied() {
74+
cmd.env(key, value);
75+
}
76+
let mut process = cmd.spawn()?;
77+
if let Some(stdin) = stdin {
78+
process.stdin.as_mut().unwrap().write_all(stdin)?;
79+
}
80+
let exit = process.wait()?;
81+
82+
const SIGNAL_EXIT_CODE: i32 = 1;
83+
Ok(exit.code().unwrap_or(SIGNAL_EXIT_CODE))
84+
}
85+
}
86+
87+
const PUSH_HOOKS: &[&str] = &[
88+
"pre-receive",
89+
"update",
90+
"post-receive",
91+
"post-update",
92+
"push-to-checkout",
93+
];

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
pub mod hooks;
12
pub mod ops;
3+
pub mod utils;

src/utils.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/// Path to a shell suitable for running hooks.
2+
pub fn git_sh() -> Option<std::path::PathBuf> {
3+
let exe_name = if cfg!(target_os = "windows") {
4+
"bash.exe"
5+
} else {
6+
"sh"
7+
};
8+
9+
if cfg!(target_os = "windows") {
10+
// Prefer git-bash since that is how git will normally be running the hooks
11+
if let Some(path) = find_git_bash() {
12+
return Some(path);
13+
}
14+
}
15+
16+
which::which(exe_name).ok()
17+
}
18+
19+
fn find_git_bash() -> Option<std::path::PathBuf> {
20+
// Git is typically installed at C:\Program Files\Git\cmd\git.exe with the cmd\ directory
21+
// in the path, however git-bash is usually not in PATH and is in bin\ directory:
22+
let git_path = which::which("git.exe").ok()?;
23+
let git_dir = git_path.parent()?.parent()?;
24+
let git_bash = git_dir.join("bin").join("bash.exe");
25+
git_bash.is_file().then(|| git_bash)
26+
}

0 commit comments

Comments
 (0)