Skip to main content

eval_magic/core/
runtime.rs

1//! Runtime helpers.
2//!
3//! The synchronous git invocation helper. No runtime asset locator lives here:
4//! the schemas are bundled into the binary at compile time (`include_str!`),
5//! `clap` owns argument parsing, and the `error: <msg>` + exit(1) contract
6//! lives in `src/main.rs`.
7
8use std::path::Path;
9use std::process::Command;
10
11/// Inherited Git routing variables that can redirect repository discovery or
12/// object/index access away from a command's current working directory.
13pub(crate) const GIT_ROUTING_ENV_VARS: &[&str] = &[
14    "GIT_DIR",
15    "GIT_WORK_TREE",
16    "GIT_INDEX_FILE",
17    "GIT_OBJECT_DIRECTORY",
18    "GIT_ALTERNATE_OBJECT_DIRECTORIES",
19    "GIT_COMMON_DIR",
20    "GIT_CEILING_DIRECTORIES",
21];
22
23/// Remove inherited repository-routing state before running a command whose
24/// current directory is intended to define its Git boundary.
25pub(crate) fn clear_git_environment(command: &mut Command) {
26    for name in GIT_ROUTING_ENV_VARS {
27        command.env_remove(name);
28    }
29}
30
31/// Outcome of a git invocation.
32///
33/// `status` is `None` when git could not be spawned at all (e.g. ENOENT, a
34/// nonexistent cwd, permission denied); the reason is surfaced into `stderr`,
35/// so callers have one channel for both git's own errors and spawn failures.
36/// `stdout` and
37/// `stderr` are raw bytes — callers that read file contents out of git
38/// (`git show`) need the undecoded buffer, not a lossy UTF-8 string.
39#[derive(Debug)]
40pub struct GitOutput {
41    pub status: Option<i32>,
42    pub stdout: Vec<u8>,
43    pub stderr: Vec<u8>,
44}
45
46/// Synchronously invoke `git` with `args` in `cwd`, returning its status and raw
47/// output. A failure to spawn git is not an error here: it yields `status: None`
48/// with the spawn error surfaced into `stderr`, so callers can handle it
49/// alongside git's own failures.
50pub fn run_git(args: &[&str], cwd: &Path) -> GitOutput {
51    let mut command = Command::new("git");
52    command.args(args).current_dir(cwd);
53    clear_git_environment(&mut command);
54    match command.output() {
55        Ok(out) => GitOutput {
56            status: out.status.code(),
57            stdout: out.stdout,
58            stderr: out.stderr,
59        },
60        Err(err) => GitOutput {
61            status: None,
62            stdout: Vec::new(),
63            stderr: format!("{err}").into_bytes(),
64        },
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    /// A successful git command returns exit status 0 and writes to stdout.
73    /// Run against this crate's own repo, which has commit history.
74    #[test]
75    fn run_git_success_status_and_stdout() {
76        let res = run_git(
77            &["rev-parse", "--short", "HEAD"],
78            env!("CARGO_MANIFEST_DIR").as_ref(),
79        );
80        assert_eq!(res.status, Some(0));
81        assert!(String::from_utf8_lossy(&res.stdout).trim().len() > 3);
82    }
83
84    /// A git command that fails (bad ref) returns a non-zero status.
85    #[test]
86    fn run_git_failing_command_nonzero() {
87        let res = run_git(
88            &["rev-parse", "not-a-real-ref-xyz"],
89            env!("CARGO_MANIFEST_DIR").as_ref(),
90        );
91        assert_ne!(res.status, Some(0));
92    }
93
94    /// When git itself cannot be spawned (here, a nonexistent cwd), the status
95    /// is `None` and the spawn error is surfaced into stderr — the contract is
96    /// the null status plus a readable reason, not any particular error-code
97    /// spelling.
98    #[test]
99    fn run_git_spawn_error_surfaced() {
100        let res = run_git(
101            &["rev-parse", "HEAD"],
102            "/nonexistent-dir-for-rungit-test".as_ref(),
103        );
104        assert_eq!(res.status, None);
105        assert!(String::from_utf8_lossy(&res.stderr).contains("No such file or directory"));
106    }
107}