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