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 // No hook runs through the wrapper, and the options follow the caller's globals; see `hooks_off`.
88 super::hooks_off::add_arguments(&mut command, arguments);
89 // `for (name, value) in overlay` destructures each borrowed pair.
90 for (name, value) in overlay {
91 command.env(name, value);
92 }
93 return command;
94}
95
96/// The base a POSIX shell and Git 2.56.0 `run-command.c` add to a signal number.
97pub const SIGNAL_EXIT_BASE: i32 = 128;
98
99/// What:
100/// Translate a child's end into this process's exit code.
101/// Why:
102/// Git 2.56.0 `run-command.c` reports a child killed by signal N as `128 + N`,
103/// which is also what a POSIX shell reports;
104/// an exit code passes through.
105///
106/// In TS you'd write (pseudocode):
107/// ```ts
108/// function exitCode(outcome: ChildOutcome): number;
109/// ```
110pub fn exit_code(outcome: ChildOutcome) -> i32 {
111 // What: `match` picks by variant and binds the number inside it.
112 // Why: The two cases are computed differently and both must be handled.
113 //
114 // In TS you'd write (pseudocode):
115 // ```ts
116 // return 'exited' in outcome ? outcome.exited : 128 + outcome.signaled;
117 // ```
118 match outcome {
119 ChildOutcome::Exited(code) => return code,
120 ChildOutcome::Signaled(signal) => return SIGNAL_EXIT_BASE + signal,
121 }
122}
123
124/// What:
125/// Read a finished child's status into a `ChildOutcome`.
126/// `#[cfg(unix)]` on the inner block compiles it only on Unix-like systems.
127/// Why:
128/// Unix reports either an exit code or a terminating signal,
129/// never both.
130/// Other
131/// systems report only an exit code.
132/// One function serves both,
133/// so the part every
134/// system shares is the part every test run exercises;
135/// a status that is neither
136/// cannot be reported faithfully and counts as a general failure.
137///
138/// In TS you'd write (pseudocode):
139/// ```ts
140/// function outcomeOf(status: { code: number | null; signal: number | null }): ChildOutcome;
141/// ```
142pub(crate) fn outcome_of(status: std::process::ExitStatus) -> ChildOutcome {
143 // `if let Some(code) = ...` runs only when the child exited normally.
144 if let Some(code) = status.code() {
145 return ChildOutcome::Exited(code);
146 }
147 #[cfg(unix)]
148 {
149 // The trait adds `.signal()` to exit statuses on Unix.
150 use std::os::unix::process::ExitStatusExt;
151 if let Some(signal) = status.signal() {
152 return ChildOutcome::Signaled(signal);
153 }
154 }
155 return ChildOutcome::Exited(1);
156}
157
158/// What:
159/// Run real Git as a child with inherited stdin,
160/// stdout and stderr,
161/// and wait.
162/// `std::io::Result<T>` is `Result<T, std::io::Error>`.
163/// Why:
164/// Callers that must do work after Git returns (post-commit policies,
165/// status
166/// notes) need the wrapper process to survive Git.
167/// The child shares the
168/// caller's terminal and streams directly;
169/// nothing is buffered or copied.
170/// This path installs no signal handlers:
171/// a signal sent to the wrapper alone
172/// does not reach Git,
173/// exactly as with the TypeScript wrapper.
174///
175/// In TS you'd write (pseudocode):
176/// ```ts
177/// async function runRealGit(realGit, args, overlay): Promise<ChildOutcome>;
178/// ```
179pub fn run_real_git(
180 real_git: &Path,
181 arguments: &[OsString],
182 overlay: &[(OsString, OsString)],
183) -> std::io::Result<ChildOutcome> {
184 // What: `.status()` starts the child with inherited streams and waits for it;
185 // a trailing `?` returns a start failure to our caller.
186 // Why: Inherited streams are the default for `.status()`, so Git reads the
187 // caller's stdin and writes the caller's stdout and stderr itself.
188 //
189 // In TS you'd write (pseudocode):
190 // ```ts
191 // const status = await spawn(realGit, args, { stdio: 'inherit' });
192 // ```
193 let status: std::process::ExitStatus = git_command(real_git, arguments, overlay).status_retrying()?;
194 // `Ok(...)` is the success variant carrying how the child ended.
195 return Ok(outcome_of(status));
196}
197
198/// What:
199/// Become real Git;
200/// returns only when Git could not be started.
201/// The returned `std::io::Error` says why the operating system refused.
202/// `#[cfg(unix)]` and `#[cfg(not(unix))]` each compile one of the two inner blocks.
203/// Why:
204/// On Unix this process is replaced by Git,
205/// which is exact forwarding:
206/// same process
207/// ID,
208/// same streams,
209/// same signals,
210/// same exit status,
211/// and no wrapper process left
212/// between the caller and Git.
213/// Windows cannot replace a process image;
214/// the nearest
215/// faithful behaviour there is to wait for Git and exit with its status.
216///
217/// In TS you'd write (pseudocode):
218/// ```ts
219/// // Unix has no Node equivalent: process.execve(realGit, args, env) replaces the process image.
220/// // Elsewhere: process.exit(exitCode(await runRealGit(realGit, args, overlay)));
221/// ```
222pub fn replace_process_with_real_git(
223 real_git: &Path,
224 arguments: &[OsString],
225 overlay: &[(OsString, OsString)],
226) -> std::io::Error {
227 #[cfg(unix)]
228 {
229 // `exec_retrying` replaces this process like `.exec()` and tries again when the start is refused.
230 return exec_retrying(&mut git_command(real_git, arguments, overlay));
231 }
232 // UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
233 #[cfg(not(unix))]
234 {
235 // `match` unpacks the run: success exits this process with Git's code.
236 match run_real_git(real_git, arguments, overlay) {
237 Ok(outcome) => std::process::exit(exit_code(outcome)),
238 Err(error) => return error,
239 }
240 }
241}
242
243/// Real-Git process controls stay out of the release executable.
244#[cfg(test)]
245#[path = "forwarding_tests.rs"]
246mod tests;