Skip to main content

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;