Skip to main content

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;