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}