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/// 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;