OXYGENHUB

Coverage / crates/oxy-core/src/repo/hooks.rs 93.9%

93 of 99 lines run, 56 of 76 functions · run #351 on main · run missed

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.
1114295×
pub fn json_str(s: &str) -> String {
1214295×
    let mut out = String::with_capacity(s.len() + 2);
1314295×
    out.push('"');
14280978×
    for c in s.chars() {
15280944×
        match c {
165×
            '"' => out.push_str("\\\""),
171×
            '\\' => out.push_str("\\\\"),
1828×
            '\n' => out.push_str("\\n"),
190×
            '\r' => out.push_str("\\r"),
200×
            '\t' => out.push_str("\\t"),
21280944×
            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
22280944×
            c => out.push(c),
23
        }
24
    }
2514295×
    out.push('"');
2614295×
    out
2714295×
}
28
292416×
pub fn json_array<I: IntoIterator<Item = String>>(items: I) -> String {
302416×
    format!("[{}]", items.into_iter().collect::<Vec<_>>().join(","))
312416×
}
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 {
412606×
    fn hook_path(&self, event: &str) -> PathBuf {
422606×
        self.oxy.join("hooks").join(event)
432606×
    }
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.
472606×
    pub fn run_hook(&self, event: &str, input: &str) -> Result<HookOutcome> {
482606×
        let path = self.hook_path(event);
492606×
        if !path.is_file() {
502596×
            return Ok(HookOutcome {
512596×
                ran: false,
522596×
                stdout: String::new(),
532596×
            });
5410×
        }
5510×
        let mut child = Command::new(&path)
5610×
            .current_dir(&self.root)
5710×
            .env("OXY_EVENT", event)
5810×
            .env("OXY_ROOT", &self.root)
5910×
            .stdin(Stdio::piped())
6010×
            .stdout(Stdio::piped())
6110×
            .stderr(Stdio::piped())
6210×
            .spawn()
6310×
            .map_err(|e| RepoError::Msg(format!("running hook {event}: {e}")))?;
6410×
        if let Some(mut stdin) = child.stdin.take() {
6510×
            let _ = stdin.write_all(input.as_bytes());
6610×
        }
6710×
        let out = child.wait_with_output()?;
6810×
        let stdout = String::from_utf8_lossy(&out.stdout).to_string();
6910×
        if !out.status.success() {
702×
            let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
712×
            return fail(format!(
72
                "hook {event} refused: {}",
732×
                if stderr.is_empty() {
740×
                    "(no message)".to_string()
75
                } else {
762×
                    stderr
77
                }
78
            ));
798×
        }
808×
        Ok(HookOutcome { ran: true, stdout })
812606×
    }
82
83
    /// Extra dependencies from the `semantic-deps` hook (spec §8.1, §20).
841×
    pub(crate) fn semantic_deps(
851×
        &self,
861×
        change_id: &ChangeId,
871×
        touched: &[String],
881×
        candidates: &[(ChangeId, ChangeHash, Vec<String>)],
891×
    ) -> Result<Vec<ChangeHash>> {
901×
        if !self.config.semantic_deps {
910×
            return Ok(vec![]);
921×
        }
931×
        let input = format!(
94
            "{{\"change_id\":{},\"touched_paths\":{},\"candidate_deps\":{}}}",
951×
            json_str(&change_id.to_hex()),
961×
            json_array(touched.iter().map(|p| json_str(p))),
972×
            json_array(candidates.iter().map(|(id, h, paths)| format!(
98
                "{{\"change_id\":{},\"hash\":{},\"paths\":{}}}",
992×
                json_str(&id.to_hex()),
1002×
                json_str(&h.to_hex()),
1013×
                json_array(paths.iter().map(|p| json_str(p)))
102
            )))
103
        );
1041×
        let out = self.run_hook("semantic-deps", &input)?;
1051×
        if !out.ran {
1060×
            return Ok(vec![]);
1071×
        }
108
        // Accept every 64-hex token after "deps"; any other shape is ignored.
1091×
        let body = out
1101×
            .stdout
1111×
            .split_once("\"deps\"")
1121×
            .map(|(_, rest)| rest.to_string())
1131×
            .unwrap_or_default();
1141×
        let mut deps = Vec::new();
1151×
        let bytes = body.as_bytes();
1161×
        let mut i = 0;
1176×
        while i + 64 <= bytes.len() {
11868×
            if bytes[i..i + 64].iter().all(|b| b.is_ascii_hexdigit())
1191×
                && (i == 0 || !bytes[i - 1].is_ascii_hexdigit())
1201×
                && (i + 64 == bytes.len() || !bytes[i + 64].is_ascii_hexdigit())
121
            {
1221×
                if let Some(h) = ObjectRef::from_hex(&body[i..i + 64]) {
1231×
                    deps.push(h);
1241×
                }
1251×
                i += 64;
1264×
            } else {
1274×
                i += 1;
1284×
            }
129
        }
1301×
        Ok(deps)
1311×
    }
132
}