1#[derive(Clone, Debug)]
2pub struct Hooks {
3 root: std::path::PathBuf,
4}
5
6impl Hooks {
7 pub fn new(hook_root: impl Into<std::path::PathBuf>) -> Self {
8 Self {
9 root: hook_root.into(),
10 }
11 }
12
13 pub fn with_repo(repo: &git2::Repository) -> Result<Self, git2::Error> {
14 let config = repo.config()?;
15 let root = config
16 .get_path("core.hooksPath")
17 .unwrap_or_else(|_| repo.path().join("hooks"));
18 Ok(Self::new(root))
19 }
20
21 pub fn root(&self) -> &std::path::Path {
22 &self.root
23 }
24
25 pub fn find_hook(&self, _repo: &git2::Repository, name: &str) -> Option<std::path::PathBuf> {
26 let mut hook_path = self.root().join(name);
27 if is_executable(&hook_path) {
28 return Some(hook_path);
29 }
30
31 if !std::env::consts::EXE_SUFFIX.is_empty() {
32 hook_path.set_extension(std::env::consts::EXE_SUFFIX);
33 if is_executable(&hook_path) {
34 return Some(hook_path);
35 }
36 }
37
38 None
42 }
43
44 pub fn run_hook(
45 &self,
46 repo: &git2::Repository,
47 name: &str,
48 args: &[&str],
49 stdin: Option<&[u8]>,
50 env: &[(&str, &str)],
51 ) -> Result<i32, std::io::Error> {
52 const SIGNAL_EXIT_CODE: i32 = 1;
53
54 let hook_path = if let Some(hook_path) = self.find_hook(repo, name) {
55 hook_path
56 } else {
57 return Ok(0);
58 };
59 let bin_name = hook_path
60 .file_name()
61 .expect("find_hook always returns a bin name")
62 .to_str()
63 .expect("find_hook always returns a utf-8 bin name");
64
65 let path = {
66 let mut path_components: Vec<std::path::PathBuf> =
67 vec![std::fs::canonicalize(self.root())?];
68 if let Some(path) = std::env::var_os(std::ffi::OsStr::new("PATH")) {
69 path_components.extend(std::env::split_paths(&path));
70 }
71 std::env::join_paths(path_components)
72 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?
73 };
74
75 let sh_path = crate::utils::git_sh().ok_or_else(|| {
76 std::io::Error::new(std::io::ErrorKind::NotFound, "No `sh` for running hooks")
77 })?;
78
79 let cwd = if PUSH_HOOKS.contains(&name) {
85 repo.path()
86 } else {
87 repo.workdir().unwrap_or_else(|| repo.path())
88 };
89
90 let mut cmd = std::process::Command::new(sh_path);
91 cmd.arg("-c")
92 .arg(format!("{bin_name} \"$@\""))
93 .arg(bin_name) .args(args)
95 .env("PATH", path)
96 .current_dir(cwd)
97 .stdin(std::process::Stdio::piped());
99 for (key, value) in env.iter().copied() {
100 cmd.env(key, value);
101 }
102 let mut process = cmd.spawn()?;
103 if let Some(stdin) = stdin {
104 use std::io::Write;
105
106 process.stdin.as_mut().unwrap().write_all(stdin)?;
107 }
108 let exit = process.wait()?;
109
110 Ok(exit.code().unwrap_or(SIGNAL_EXIT_CODE))
111 }
112
113 pub fn run_post_rewrite_rebase(
124 &self,
125 repo: &git2::Repository,
126 changed_oids: &[(git2::Oid, git2::Oid)],
127 ) {
128 let name = "post-rewrite";
129 let command = "rebase";
130 let args = [command];
131 let mut stdin = String::new();
132 for (old_oid, new_oid) in changed_oids {
133 use std::fmt::Write;
134 writeln!(stdin, "{old_oid} {new_oid}").expect("Always writeable");
135 }
136
137 match self.run_hook(repo, name, &args, Some(stdin.as_bytes()), &[]) {
138 Ok(0) => {}
139 Ok(code) => {
140 log::trace!("Hook `{}` failed with code {}", name, code);
141 }
142 Err(err) => {
143 log::trace!("Hook `{}` failed with {}", name, err);
144 }
145 }
146 }
147
148 pub fn run_reference_transaction<'t>(
155 &'t self,
156 repo: &'t git2::Repository,
157 changed_refs: &'t [(git2::Oid, git2::Oid, &'t str)],
158 ) -> Result<ReferenceTransaction<'t>, std::io::Error> {
159 self.run_reference_transaction_prepare(repo, changed_refs)?;
160
161 Ok(ReferenceTransaction {
162 hook: self,
163 repo,
164 changed_refs,
165 })
166 }
167
168 pub fn run_reference_transaction_prepare(
181 &self,
182 repo: &git2::Repository,
183 changed_refs: &[(git2::Oid, git2::Oid, &str)],
184 ) -> Result<(), std::io::Error> {
185 let name = "reference-transaction";
186 let state = "prepare";
187 let args = [state];
188 let mut stdin = String::new();
189 for (old_oid, new_oid, ref_name) in changed_refs {
190 use std::fmt::Write;
191 writeln!(stdin, "{old_oid} {new_oid} {ref_name}").expect("Always writeable");
192 }
193
194 let code = self.run_hook(repo, name, &args, Some(stdin.as_bytes()), &[])?;
195 if code == 0 {
196 Ok(())
197 } else {
198 log::trace!("Hook `{}` failed with code {}", name, code);
199 Err(std::io::Error::new(
200 std::io::ErrorKind::Interrupted,
201 format!("`{name}` hook failed with code {code}"),
202 ))
203 }
204 }
205
206 pub fn run_reference_transaction_committed(
213 &self,
214 repo: &git2::Repository,
215 changed_refs: &[(git2::Oid, git2::Oid, &str)],
216 ) {
217 let name = "reference-transaction";
218 let state = "committed";
219 let args = [state];
220 let mut stdin = String::new();
221 for (old_oid, new_oid, ref_name) in changed_refs {
222 use std::fmt::Write;
223 writeln!(stdin, "{old_oid} {new_oid} {ref_name}").expect("Always writeable");
224 }
225
226 match self.run_hook(repo, name, &args, Some(stdin.as_bytes()), &[]) {
227 Ok(0) => {}
228 Ok(code) => {
229 log::trace!("Hook `{}` failed with code {}", name, code);
230 }
231 Err(err) => {
232 log::trace!("Hook `{}` failed with {}", name, err);
233 }
234 }
235 }
236
237 pub fn run_reference_transaction_aborted(
244 &self,
245 repo: &git2::Repository,
246 changed_refs: &[(git2::Oid, git2::Oid, &str)],
247 ) {
248 let name = "reference-transaction";
249 let state = "aborted";
250 let args = [state];
251 let mut stdin = String::new();
252 for (old_oid, new_oid, ref_name) in changed_refs {
253 use std::fmt::Write;
254 writeln!(stdin, "{old_oid} {new_oid} {ref_name}").expect("Always writeable");
255 }
256
257 match self.run_hook(repo, name, &args, Some(stdin.as_bytes()), &[]) {
258 Ok(0) => {}
259 Ok(code) => {
260 log::trace!("Hook `{}` failed with code {}", name, code);
261 }
262 Err(err) => {
263 log::trace!("Hook `{}` failed with {}", name, err);
264 }
265 }
266 }
267}
268
269pub struct ReferenceTransaction<'t> {
270 hook: &'t Hooks,
271 repo: &'t git2::Repository,
272 changed_refs: &'t [(git2::Oid, git2::Oid, &'t str)],
273}
274
275impl ReferenceTransaction<'_> {
276 pub fn committed(self) {
277 let Self {
278 hook,
279 repo,
280 changed_refs,
281 } = self;
282 hook.run_reference_transaction_committed(repo, changed_refs);
283 }
284
285 pub fn aborted(self) {
286 let Self {
287 hook,
288 repo,
289 changed_refs,
290 } = self;
291 hook.run_reference_transaction_aborted(repo, changed_refs);
292 }
293}
294
295impl Drop for ReferenceTransaction<'_> {
296 fn drop(&mut self) {
297 self.hook
298 .run_reference_transaction_aborted(self.repo, self.changed_refs);
299 }
300}
301
302const PUSH_HOOKS: &[&str] = &[
303 "pre-receive",
304 "update",
305 "post-receive",
306 "post-update",
307 "push-to-checkout",
308];
309
310#[cfg(unix)]
311fn is_executable(path: &std::path::Path) -> bool {
312 use std::os::unix::fs::PermissionsExt;
313
314 let metadata = match path.metadata() {
315 Ok(metadata) => metadata,
316 Err(_) => return false,
317 };
318 let permissions = metadata.permissions();
319 metadata.is_file() && permissions.mode() & 0o111 != 0
320}
321
322#[cfg(not(unix))]
323fn is_executable(path: &std::path::Path) -> bool {
324 path.is_file()
325}