Skip to main content

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