1use anyhow::Result;
2use std::process::Command;
3
4use crate::opener::augmented_path;
5
6pub struct HookContext {
8 pub owner: String,
10 pub repo: String,
12 pub issue: String,
14 pub branch: String,
16 pub worktree_path: String,
18}
19
20impl HookContext {
21 #[must_use]
24 pub fn render(&self, template: &str) -> String {
25 template
26 .replace("{{owner}}", &self.owner)
27 .replace("{{repo}}", &self.repo)
28 .replace("{{issue}}", &self.issue)
29 .replace("{{branch}}", &self.branch)
30 .replace("{{worktree_path}}", &self.worktree_path)
31 }
32}
33
34pub fn run_hook(script: &str, ctx: &HookContext) -> Result<()> {
43 let rendered = ctx.render(script);
44
45 let ext = if cfg!(windows) { ".bat" } else { ".sh" };
46 let tmp_path =
47 std::env::temp_dir().join(format!("worktree-hook-{}{ext}", uuid::Uuid::new_v4()));
48 std::fs::write(&tmp_path, rendered.as_bytes())?;
49
50 #[cfg(unix)]
51 {
52 use std::os::unix::fs::PermissionsExt;
53 std::fs::set_permissions(&tmp_path, std::fs::Permissions::from_mode(0o755))?;
54 }
55
56 #[cfg(windows)]
57 let result = Command::new("cmd")
58 .args([std::ffi::OsStr::new("/C"), tmp_path.as_os_str()])
59 .env("PATH", augmented_path())
60 .status();
61 #[cfg(not(windows))]
62 let result = Command::new("sh")
63 .arg(&tmp_path)
64 .env("PATH", augmented_path())
65 .status();
66 let _ = std::fs::remove_file(&tmp_path);
67
68 match result {
69 Ok(status) if !status.success() => {
70 eprintln!("Warning: hook exited with status {:?}", status.code());
71 }
72 Err(e) => {
74 eprintln!("Warning: failed to run hook: {e}");
75 }
76 _ => {}
78 }
79
80 Ok(())
81}
82
83#[cfg(test)]
84#[path = "hooks_tests.rs"]
85mod tests;
86
87#[cfg(test)]
88#[path = "hooks_multiline_tests.rs"]
89mod multiline_tests;