git_policy_cli/git_metadata.rs
1//! What:
2//! Run a read-only real-Git query and capture its output as raw bytes.
3//! Why:
4//! Repository facts (top level,
5//! Git directory) come from Git itself,
6//! and the
7//! paths it prints need not be UTF-8.
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! // const { stdout } = await spawn(gitPath, ['rev-parse', '--git-dir'], { stdout: 'pipe' });
12//! ```
13
14/// Import the shared child-command builder.
15use super::forwarding::git_command;
16/// What:
17/// `OsString` is owned operating-system text of raw OS bytes (sibling `String`
18/// must be UTF-8).
19/// Why:
20/// Query arguments include the caller's unchanged global options.
21///
22/// In TS you'd write (pseudocode):
23/// ```ts
24/// // string[] argv, but byte-preserving.
25/// ```
26use std::ffi::OsString;
27/// `Path`/`PathBuf` are borrowed/owned filesystem paths of raw OS bytes.
28use std::path::{Path, PathBuf};
29/// `Stdio` selects where a child's stream is connected.
30use std::process::Stdio;
31
32/// What:
33/// What one metadata query produced.
34/// `Vec<u8>` is an owned byte list (`u8` is one byte);
35/// sibling `String` would
36/// require UTF-8.
37/// Why:
38/// Git prints paths as raw bytes,
39/// and a failed query is an ordinary answer
40/// ("not a repository"),
41/// not an error.
42///
43/// In TS you'd write (pseudocode):
44/// ```ts
45/// type MetadataOutput = { success: boolean; code: number | null; stdout: Buffer; stderr: Buffer };
46/// ```
47#[derive(Clone, Debug, Eq, PartialEq)]
48pub struct MetadataOutput {
49 /// Whether Git exited with status zero.
50 pub success: bool,
51 /// Git's exit code,
52 /// or nothing when a signal ended it.
53 /// Some queries answer through
54 /// their exit code alone (`git diff-index --quiet` exits 1 for "differs").
55 pub code: Option<i32>,
56 /// Exact bytes Git wrote to standard output,
57 /// including what it printed before failing.
58 pub stdout: Vec<u8>,
59 /// Exact bytes Git wrote to standard error.
60 pub stderr: Vec<u8>,
61}
62
63/// What:
64/// Run real Git with captured output and no standard input.
65/// `std::io::Result<T>` is `Result<T, std::io::Error>`.
66/// Why:
67/// A query must never consume the caller's stdin (which belongs to the
68/// forwarded command) or write to the caller's streams.
69///
70/// In TS you'd write (pseudocode):
71/// ```ts
72/// async function runMetadataGit(realGit, args, overlay): Promise<MetadataOutput>;
73/// ```
74pub fn run_metadata_git(
75 real_git: &Path,
76 arguments: &[OsString],
77 overlay: &[(OsString, OsString)],
78) -> std::io::Result<MetadataOutput> {
79 // What: `.stdin(Stdio::null())` connects the child's input to nothing; `.output()`
80 // captures stdout and stderr and waits. A trailing `?` returns a start failure.
81 // Why: Only "Git could not be started" is an error here.
82 //
83 // In TS you'd write (pseudocode):
84 // ```ts
85 // const result = await spawn(realGit, args, { stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' });
86 // ```
87 let output: std::process::Output = git_command(real_git, arguments, overlay)
88 .stdin(Stdio::null())
89 .output()?;
90 // `Ok(...)` is the success variant carrying the captured result.
91 return Ok(MetadataOutput {
92 success: output.status.success(),
93 code: output.status.code(),
94 stdout: output.stdout,
95 stderr: output.stderr,
96 });
97}
98
99/// What:
100/// Remove exactly one final line break (`\n` or `\r\n`) from Git output.
101/// `&[u8]` borrows bytes;
102/// the result borrows a shorter view of the same bytes.
103/// Why:
104/// Git terminates each printed value with one line break;
105/// anything before it,
106/// including other line breaks,
107/// is part of the value.
108///
109/// In TS you'd write (pseudocode):
110/// ```ts
111/// function stripGitLine(output: Buffer): Buffer;
112/// ```
113pub fn strip_git_line(output: &[u8]) -> &[u8] {
114 // What: `.strip_suffix(..)` is `Some(shorter)` when the bytes end with the suffix.
115 // Why: The two-byte ending is tried first so its `\r` is not left behind.
116 //
117 // In TS you'd write (pseudocode):
118 // ```ts
119 // if (output.endsWith('\r\n')) return output.slice(0, -2);
120 // ```
121 if let Some(stripped) = output.strip_suffix(b"\r\n") {
122 return stripped;
123 }
124 if let Some(stripped) = output.strip_suffix(b"\n") {
125 return stripped;
126 }
127 return output;
128}
129
130/// What:
131/// Turn path bytes printed by Git into a path,
132/// without decoding on Unix.
133/// `Option<PathBuf>` is "an owned path or nothing".
134/// `#[cfg(unix)]` and
135/// `#[cfg(not(unix))]` each compile one of the two inner blocks.
136/// Why:
137/// Unix paths are arbitrary bytes and must round-trip exactly;
138/// other systems can
139/// only build a path from valid UTF-8.
140/// An empty value is never a path anywhere.
141///
142/// In TS you'd write (pseudocode):
143/// ```ts
144/// function pathFromGitBytes(bytes: Buffer): string | undefined;
145/// ```
146pub fn path_from_git_bytes(bytes: &[u8]) -> Option<PathBuf> {
147 if bytes.is_empty() {
148 // `None` is the "absent" variant.
149 return None;
150 }
151 #[cfg(unix)]
152 {
153 // The trait adds `from_vec`, which wraps raw bytes as OS text unchanged.
154 use std::os::unix::ffi::OsStringExt;
155 // `.to_vec()` copies the borrowed bytes into an owned list.
156 return Some(PathBuf::from(OsString::from_vec(bytes.to_vec())));
157 }
158 #[cfg(not(unix))]
159 {
160 // `match` unpacks the decoding: text becomes a path, other bytes are not one.
161 match std::str::from_utf8(bytes) {
162 Ok(text) => return Some(PathBuf::from(text)),
163 Err(_) => return None,
164 }
165 }
166}
167
168/// Byte-level controls stay out of the release executable.
169#[cfg(test)]
170#[path = "git_metadata_tests.rs"]
171mod tests;