Skip to main content

worktree_io/
hooks.rs

1use anyhow::Result;
2use std::process::Command;
3
4use crate::opener::augmented_path;
5
6pub struct HookContext {
7    pub owner: String,
8    pub repo: String,
9    pub issue: String,
10    pub branch: String,
11    pub worktree_path: String,
12}
13
14impl HookContext {
15    pub fn render(&self, template: &str) -> String {
16        template
17            .replace("{{owner}}", &self.owner)
18            .replace("{{repo}}", &self.repo)
19            .replace("{{issue}}", &self.issue)
20            .replace("{{branch}}", &self.branch)
21            .replace("{{worktree_path}}", &self.worktree_path)
22    }
23}
24
25/// Render `script` with `ctx`, write to a temp file, and execute it.
26/// Stdout and stderr are forwarded to the caller's terminal.
27/// A non-zero exit code prints a warning but does not return an error.
28pub fn run_hook(script: &str, ctx: &HookContext) -> Result<()> {
29    let rendered = ctx.render(script);
30
31    let tmp_path = std::env::temp_dir().join(format!("worktree-hook-{}.sh", std::process::id()));
32    std::fs::write(&tmp_path, rendered.as_bytes())?;
33
34    #[cfg(unix)]
35    {
36        use std::os::unix::fs::PermissionsExt;
37        std::fs::set_permissions(&tmp_path, std::fs::Permissions::from_mode(0o755))?;
38    }
39
40    let result = Command::new("sh")
41        .arg(&tmp_path)
42        .env("PATH", augmented_path())
43        .status();
44    let _ = std::fs::remove_file(&tmp_path);
45
46    match result {
47        Ok(status) if !status.success() => {
48            eprintln!("Warning: hook exited with status {:?}", status.code());
49        }
50        // LLVM_COV_EXCL_START
51        Err(e) => {
52            eprintln!("Warning: failed to run hook: {e}");
53        }
54        // LLVM_COV_EXCL_STOP
55        _ => {}
56    }
57
58    Ok(())
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    fn ctx() -> HookContext {
65        HookContext {
66            owner: "acme".into(),
67            repo: "api".into(),
68            issue: "42".into(),
69            branch: "issue-42".into(),
70            worktree_path: "/tmp/wt".into(),
71        }
72    }
73    #[test]
74    fn test_render_all_placeholders() {
75        let out = ctx().render("{{owner}}/{{repo}}#{{issue}} {{branch}} {{worktree_path}}");
76        assert_eq!(out, "acme/api#42 issue-42 /tmp/wt");
77    }
78    #[test]
79    fn test_render_no_placeholders() {
80        assert_eq!(ctx().render("hello"), "hello");
81    }
82    #[test]
83    fn test_run_hook_success() {
84        run_hook("true", &ctx()).unwrap();
85    }
86    #[test]
87    fn test_run_hook_nonzero_exit() {
88        run_hook("exit 1", &ctx()).unwrap();
89    }
90}