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