OXYGENHUB
# 957fee30826e oxygen / crates / oxy-core / src / repo / hooks.rs
4.2 KiB · Rust · last changed Hooks: pre-commit, post-commit, pre-push, post-fetch, post-checkout with JSON payloads; semantic-deps hook behind hooks.semantic-deps (spec 0.6k) 1 month ago source blame history raw
1
//! Client hooks (spec §20): executables in `.oxy/hooks/<event>`, never
2
//! cloned. Each receives one JSON object on stdin; `pre-*` hooks block the
3
//! operation by exiting non-zero, and their stderr becomes the error. The
4
//! `semantic-deps` hook runs only when `hooks.semantic-deps = true` and
5
//! answers `{"deps": ["<hash hex>", ...]}`.
6
7
use super::*;
8
use std::process::{Command, Stdio};
9
10
/// Minimal JSON string escaping.
11
pub fn json_str(s: &str) -> String {
12
    let mut out = String::with_capacity(s.len() + 2);
13
    out.push('"');
14
    for c in s.chars() {
15
        match c {
16
            '"' => out.push_str("\\\""),
17
            '\\' => out.push_str("\\\\"),
18
            '\n' => out.push_str("\\n"),
19
            '\r' => out.push_str("\\r"),
20
            '\t' => out.push_str("\\t"),
21
            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
22
            c => out.push(c),
23
        }
24
    }
25
    out.push('"');
26
    out
27
}
28
29
pub fn json_array<I: IntoIterator<Item = String>>(items: I) -> String {
30
    format!("[{}]", items.into_iter().collect::<Vec<_>>().join(","))
31
}
32
33
/// What running a hook produced.
34
#[derive(Debug, Clone, PartialEq, Eq)]
35
pub struct HookOutcome {
36
    pub ran: bool,
37
    pub stdout: String,
38
}
39
40
impl Repo {
41
    fn hook_path(&self, event: &str) -> PathBuf {
42
        self.oxy.join("hooks").join(event)
43
    }
44
45
    /// Run the hook for `event` with `input` on stdin, if one exists and is
46
    /// executable. A non-zero exit is an error carrying the hook's stderr.
47
    pub fn run_hook(&self, event: &str, input: &str) -> Result<HookOutcome> {
48
        let path = self.hook_path(event);
49
        if !path.is_file() {
50
            return Ok(HookOutcome { ran: false, stdout: String::new() });
51
        }
52
        let mut child = Command::new(&path)
53
            .current_dir(&self.root)
54
            .env("OXY_EVENT", event)
55
            .env("OXY_ROOT", &self.root)
56
            .stdin(Stdio::piped())
57
            .stdout(Stdio::piped())
58
            .stderr(Stdio::piped())
59
            .spawn()
60
            .map_err(|e| RepoError::Msg(format!("running hook {event}: {e}")))?;
61
        if let Some(mut stdin) = child.stdin.take() {
62
            let _ = stdin.write_all(input.as_bytes());
63
        }
64
        let out = child.wait_with_output()?;
65
        let stdout = String::from_utf8_lossy(&out.stdout).to_string();
66
        if !out.status.success() {
67
            let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
68
            return fail(format!("hook {event} refused: {}", if stderr.is_empty() { "(no message)".to_string() } else { stderr }));
69
        }
70
        Ok(HookOutcome { ran: true, stdout })
71
    }
72
73
    /// Extra dependencies from the `semantic-deps` hook (spec §8.1, §20).
74
    pub(crate) fn semantic_deps(&self, change_id: &ChangeId, touched: &[String], candidates: &[(ChangeId, ChangeHash, Vec<String>)]) -> Result<Vec<ChangeHash>> {
75
        if !self.config.semantic_deps {
76
            return Ok(vec![]);
77
        }
78
        let input = format!(
79
            "{{\"change_id\":{},\"touched_paths\":{},\"candidate_deps\":{}}}",
80
            json_str(&change_id.to_hex()),
81
            json_array(touched.iter().map(|p| json_str(p))),
82
            json_array(candidates.iter().map(|(id, h, paths)| format!("{{\"change_id\":{},\"hash\":{},\"paths\":{}}}", json_str(&id.to_hex()), json_str(&h.to_hex()), json_array(paths.iter().map(|p| json_str(p))))))
83
        );
84
        let out = self.run_hook("semantic-deps", &input)?;
85
        if !out.ran {
86
            return Ok(vec![]);
87
        }
88
        // Accept every 64-hex token after "deps"; any other shape is ignored.
89
        let body = out.stdout.split_once("\"deps\"").map(|(_, rest)| rest.to_string()).unwrap_or_default();
90
        let mut deps = Vec::new();
91
        let bytes = body.as_bytes();
92
        let mut i = 0;
93
        while i + 64 <= bytes.len() {
94
            if bytes[i..i + 64].iter().all(|b| b.is_ascii_hexdigit()) && (i == 0 || !bytes[i - 1].is_ascii_hexdigit()) && (i + 64 == bytes.len() || !bytes[i + 64].is_ascii_hexdigit()) {
95
                if let Some(h) = ObjectRef::from_hex(&body[i..i + 64]) {
96
                    deps.push(h);
97
                }
98
                i += 64;
99
            } else {
100
                i += 1;
101
            }
102
        }
103
        Ok(deps)
104
    }
105
}