Skip to main content

git_policy_cli/invocation/
forwarding.rs

1//! What:
2//!  Start real Git with the caller's exact arguments,
3//!  streams and environment.
4//! Why:
5//!  The wrapper adds policy around Git;
6//!  it must never change what Git receives
7//!      or what the caller observes from Git.
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! // spawn(gitPath, args, { stdio: 'inherit', env }) and exit with the child's status.
12//! ```
13
14/// What:
15///  `OsString` is owned operating-system text of raw OS bytes (sibling `String`
16///       must be UTF-8).
17/// Why:
18///   Arguments are forwarded byte for byte;
19///  they are never decoded or re-quoted.
20///
21/// In TS you'd write (pseudocode):
22/// ```ts
23/// // process.argv entries, but without any UTF-8 decoding.
24/// ```
25use std::ffi::OsString;
26/// `Path` is a borrowed filesystem path of raw OS bytes.
27use std::path::Path;
28/// What:
29///  The marker every real-Git start carries, and the two ways a start can be made.
30/// Why:
31///   A cli-git executable that is started as "Git" finds the marker and stops instead of starting one more wrapper.
32use crate::started_as_git::{StartMode, mark_real_git_start};
33/// Starts that ride out a short shortage of processes.
34#[cfg(unix)]
35use crate::start_retry::exec_retrying;
36/// Starts that ride out a short shortage of processes.
37use crate::start_retry::RetryStart;
38/// `Command` builds a child process from a program and an argument list,
39///  with no shell.
40use std::process::Command;
41
42/// What:
43///  How a Git child ended.
44///       `i32` is a signed 32-bit integer,
45///  the type of exit codes and signal numbers
46///       (siblings `u8`,
47///  `i64`).
48/// Why:
49///   A child killed by a signal has no exit code;
50///  callers need both cases to
51///       report Git's result faithfully.
52///  `i32` is what the operating system returns.
53///
54/// In TS you'd write (pseudocode):
55/// ```ts
56/// type ChildOutcome = { exited: number } | { signaled: number };
57/// ```
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
59pub enum ChildOutcome {
60    /// The child called `exit` with this code.
61    Exited(i32),
62    /// The child was terminated by this signal number.
63    Signaled(i32),
64}
65
66/// What:
67///  Build the child command:
68///  program,
69///  unchanged arguments,
70///  added variables.
71///       `&[OsString]` borrows the argument list;
72///  `&[(OsString, OsString)]` borrows
73///       name/value pairs.
74/// Why:
75///   One builder guarantees every real-Git start,
76///  forwarded or queried,
77///  uses an
78///       argument array (no shell string) and inherits the caller's environment plus
79///       only the overlay.
80///
81/// In TS you'd write (pseudocode):
82/// ```ts
83/// function gitCommand(realGit: string, args: string[], overlay: [string, string][]): Command;
84/// ```
85pub fn git_command(
86    real_git: &Path,
87    arguments: &[OsString],
88    overlay: &[(OsString, OsString)],
89) -> Command {
90    // `mut` allows the builder to be configured step by step.
91    let mut command: Command = Command::new(real_git);
92    // No hook runs through the wrapper, and the options follow the caller's globals; see `hooks_off`.
93    crate::hooks_off::add_arguments(&mut command, arguments);
94    // `for (name, value) in overlay` destructures each borrowed pair.
95    for (name, value) in overlay {
96        command.env(name, value);
97    }
98    // Every real-Git start says it is one, so a cli-git executable that arrives as "Git" stops (`started_as_git.rs`).
99    mark_real_git_start(&mut command, StartMode::Child);
100    return command;
101}
102
103/// The base a POSIX shell and Git 2.56.0 `run-command.c` add to a signal number.
104pub const SIGNAL_EXIT_BASE: i32 = 128;
105
106/// What:
107///  Translate a child's end into this process's exit code.
108/// Why:
109///   Git 2.56.0 `run-command.c` reports a child killed by signal N as `128 + N`,
110///       which is also what a POSIX shell reports;
111///  an exit code passes through.
112///
113/// In TS you'd write (pseudocode):
114/// ```ts
115/// function exitCode(outcome: ChildOutcome): number;
116/// ```
117pub fn exit_code(outcome: ChildOutcome) -> i32 {
118    // What: `match` picks by variant and binds the number inside it.
119    // Why:  The two cases are computed differently and both must be handled.
120    //
121    // In TS you'd write (pseudocode):
122    // ```ts
123    // return 'exited' in outcome ? outcome.exited : 128 + outcome.signaled;
124    // ```
125    match outcome {
126        ChildOutcome::Exited(code) => return code,
127        ChildOutcome::Signaled(signal) => return SIGNAL_EXIT_BASE + signal,
128    }
129}
130
131/// What:
132///  Read a finished child's status into a `ChildOutcome`.
133///       `#[cfg(unix)]` on the inner block compiles it only on Unix-like systems.
134/// Why:
135///   Unix reports either an exit code or a terminating signal,
136///  never both.
137///  Other
138///       systems report only an exit code.
139///  One function serves both,
140///  so the part every
141///       system shares is the part every test run exercises;
142///  a status that is neither
143///       cannot be reported faithfully and counts as a general failure.
144///
145/// In TS you'd write (pseudocode):
146/// ```ts
147/// function outcomeOf(status: { code: number | null; signal: number | null }): ChildOutcome;
148/// ```
149pub(crate) fn outcome_of(status: std::process::ExitStatus) -> ChildOutcome {
150    // `if let Some(code) = ...` runs only when the child exited normally.
151    if let Some(code) = status.code() {
152        return ChildOutcome::Exited(code);
153    }
154    #[cfg(unix)]
155    {
156        // The trait adds `.signal()` to exit statuses on Unix.
157        use std::os::unix::process::ExitStatusExt;
158        if let Some(signal) = status.signal() {
159            return ChildOutcome::Signaled(signal);
160        }
161    }
162    return ChildOutcome::Exited(1);
163}
164
165/// What:
166///  Run real Git as a child with inherited stdin,
167///  stdout and stderr,
168///  and wait.
169///       `std::io::Result<T>` is `Result<T, std::io::Error>`.
170/// Why:
171///   Callers that must do work after Git returns (post-commit policies,
172///  status
173///       notes) need the wrapper process to survive Git.
174///  The child shares the
175///       caller's terminal and streams directly;
176///  nothing is buffered or copied.
177///       This path installs no signal handlers:
178///  a signal sent to the wrapper alone
179///       does not reach Git,
180///  exactly as with the TypeScript wrapper.
181///
182/// In TS you'd write (pseudocode):
183/// ```ts
184/// async function runRealGit(realGit, args, overlay): Promise<ChildOutcome>;
185/// ```
186pub fn run_real_git(
187    real_git: &Path,
188    arguments: &[OsString],
189    overlay: &[(OsString, OsString)],
190) -> std::io::Result<ChildOutcome> {
191    // What: `.status()` starts the child with inherited streams and waits for it;
192    //       a trailing `?` returns a start failure to our caller.
193    // Why:  Inherited streams are the default for `.status()`, so Git reads the
194    //       caller's stdin and writes the caller's stdout and stderr itself.
195    //
196    // In TS you'd write (pseudocode):
197    // ```ts
198    // const status = await spawn(realGit, args, { stdio: 'inherit' });
199    // ```
200    let status: std::process::ExitStatus = git_command(real_git, arguments, overlay).status_retrying()?;
201    // `Ok(...)` is the success variant carrying how the child ended.
202    return Ok(outcome_of(status));
203}
204
205/// What:
206///  Become real Git;
207///  returns only when Git could not be started.
208///       The returned `std::io::Error` says why the operating system refused.
209///       `#[cfg(unix)]` and `#[cfg(not(unix))]` each compile one of the two inner blocks.
210/// Why:
211///   On Unix this process is replaced by Git,
212///  which is exact forwarding:
213///  same process
214///       ID,
215///  same streams,
216///  same signals,
217///  same exit status,
218///  and no wrapper process left
219///       between the caller and Git.
220///  Windows cannot replace a process image;
221///  the nearest
222///       faithful behaviour there is to wait for Git and exit with its status.
223///
224/// In TS you'd write (pseudocode):
225/// ```ts
226/// // Unix has no Node equivalent: process.execve(realGit, args, env) replaces the process image.
227/// // Elsewhere: process.exit(exitCode(await runRealGit(realGit, args, overlay)));
228/// ```
229pub fn replace_process_with_real_git(
230    real_git: &Path,
231    arguments: &[OsString],
232    overlay: &[(OsString, OsString)],
233) -> std::io::Error {
234    #[cfg(unix)]
235    {
236        let mut command: Command = git_command(real_git, arguments, overlay);
237        // The program replaces this very process, so the marker says "replace" instead of the builder's "child".
238        mark_real_git_start(&mut command, StartMode::Replace);
239        // `exec_retrying` replaces this process like `.exec()` and tries again when the start is refused.
240        return exec_retrying(&mut command);
241    }
242    // UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
243    #[cfg(not(unix))]
244    {
245        // `match` unpacks the run: success exits this process with Git's code.
246        match run_real_git(real_git, arguments, overlay) {
247            Ok(outcome) => std::process::exit(exit_code(outcome)),
248            Err(error) => return error,
249        }
250    }
251}
252
253/// Real-Git process controls stay out of the release executable.
254#[cfg(test)]
255#[path = "forwarding_tests.rs"]
256mod tests;