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